Основы Windows Presentation Foundation Крис Андерсон Отзывы Будучи одним из архитекторов WPF, Крис Андерсон со знанием дела объясняет не только «как», но и «почему». Эта книга – отличный источник информации для любо# го, кто хочет понять принципы проектирования и эффективного применения WPF. – Андерс Хейльсберг, технический специалист, корпорация Майкрософт Если считать, что WPF – технология создания пользовательских интерфейсов для следующего поколения систем Windows, то Крис Андерсон будет играть роль Чарльза Петцольда для разработчиков следующего поколения интерфейсов. – Тэд Ньюард, основатель и редактор сайта TheServerSide.NET Отличная книга, которая не только является прекрасным введением в техноло# гию WPF, но и объясняет, как раскрыть огромный потенциал, заложенный в ней. – Скотт Гатри, генеральный директор подразделения разработок, корпорация Майкрософт Технология WPF – совершенно новый подход к созданию графических при# ложений, основанный на принципах, заложенных в Windows Forms и Web. Крис не только замечательно объяснил, как пользоваться новыми возможностями WPF (иллюстрируя текст примерами программ и XAML#кода), но и почему все устроено именно так, а не иначе. Как один их архитекторов, Крис хорошо знаком с внутренним устройством и принципами проектирования WPF, а равно с тон# костями написания программ на базе этой технологии. Это очень важно знать, ес# ли вы собираетесь серьезно применять ее в своих разработках. – Брайан Нойес, главный архитектор, IDesign Inc., региональный директор Microsoft, диплом Microsoft MVP Мне была предоставлена возможность познакомиться с книгой Криса Андер# сона, и я считаю ее исключительно ценным источником, который, не кривя ду# шой, могу рекомендовать. Лично я, сталкиваясь с новой технологией, всегда стремлюсь понять, как она связана с теми, которые призвана заменить. Крис в са# мом начале соотносит WPF с миром пользовательских интерфейсов, написанных на C++ для 32#разрядных версий Windows. Крис демонстрирует глубокое пони# мание принципов WPF, показывает, как эта технология работает, и помогает чи# тателю воспользоваться уже имеющимися у него знаниями, для чего приводит примеры, имитирующие передовые приложения. & Билл Шелдон, главный инженер, компания InterKnowlogy. Посвящается моей жене Меган, которая всегда рядом, поддерживает меня во всех начинаниях и вдохновляет на новые достижения. Крис Андерсон Основы Windows Presentation Foundation Essential Windows Presentation Foundation Chris Anderson Addison Wesley Upper Saddle River, NJ • Boston • Indianapolis • San FranciscoNew York • Toronto • Montreal • London • Mьnich • Paris • MadridCapetown • Sydney • Tokyo • Singapore • Mexico City Основы Windows Presentation Foundation Крис Андерсон Москва, 2008 УДК ББК А65 004.4 32.973.26 018.2 А65 К. Андерсон Основы Windows Presentation Foundation. Пер. с англ. А. Слинкина — М.: ДМК Пресс, 2008 — 432 с.: ил. ISBN 5 94074 363 3 В книге излагаются принципиальные основы новой платформы для построения графических интерфейсов пользователя Windows Presentation Foundation (WPF), которая является составной частью каркаса .NET Framework 3.0 и включена в дистрибутив Windows Vista. Являясь одним из архитекторов системы, автор со знанием дела рассказывает о том, почему были приняты те или иные решения и как их следует применять на практике. Хотя эта книга не ставит себе целью ответить на все практические вопросы разработки приложений, она станет незаменимым спутником серьезного программиста, желающего глубоко разобраться в новой технологии. УДК 004.4 ББК 32.973.26 018.2 Original Eglish language edition publihed by Pearson Education, Inc. Copyright © 2007 by Pearson Education, Inc. All rights reserved. Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. Материал, изложенный в данной книге, многократно проверен. Но поскольку вероятность технических ошибок все равно существует, издательство не может гарантировать абсолютную точность и правильность приводимых сведений. В связи с этим издательство не несет ответственности за возможные ошибки, связанные с использованием книги. ISBN 13: 978#0#321#37447#9 (англ.) ISBN 10: 0#321#37447#9 (англ.) ISBN 5#94074#363#3 (рус.) © 2008 Copyright. Pearson Education, Inc. © Перевод на русский язык, оформление, ДМК Пресс, 2008 7 Оглавление Предисловие ................................................................................................................................12 Предисловие ................................................................................................................................13 Вступление....................................................................................................................................15 Краткий экскурс в историю графических интерфейсов пользователя...............15 Принципы WPF ..........................................................................................................................17 Создать платформу для развитой презентации ........................................................17 Создать программируемую платформу........................................................................17 Создать декларативную платформу ..............................................................................18 Интегрировать пользовательский интерфейс, документы и мультимедиа.....18 Взять лучшее из Web и лучшее из Windows..............................................................18 Свести программистов и дизайнеров ............................................................................18 Что собой представляет эта книга ......................................................................................19 Предварительные условия ................................................................................................19 Организация...........................................................................................................................20 Благодарности.............................................................................................................................20 Об авторе.......................................................................................................................................21 Глава 1. Введение ......................................................................................................................23 WPF как новый ГИП................................................................................................................23 Библиотека User32 в стиле книги Чарльза Петцольда ..........................................23 HTML или, по#другому, Web...........................................................................................29 Краткое знакомство с моделью программирования XAML ....................................32 Обзор WPF ..................................................................................................................................38 С чего начать ..........................................................................................................................38 Переходим к разметке.........................................................................................................40 Основы .....................................................................................................................................41 Работа с данными .................................................................................................................47 Развитая интеграция ...........................................................................................................50 Будем стильными .................................................................................................................57 Инструменты для построения приложения ...............................................................59 Чего мы достигли .......................................................................................................................60 8 Оглавление Глава 2. Приложения................................................................................................................61 Принципы организации приложения.................................................................................61 Масштабируемые приложения........................................................................................61 Стиль Web...............................................................................................................................65 Стиль персональных приложений .................................................................................67 Объект Application ....................................................................................................................69 Определение...........................................................................................................................70 Время жизни ..........................................................................................................................72 Обработка ошибок ...............................................................................................................73 Управление состоянием .....................................................................................................75 Ресурсы и конфигурирование ..............................................................................................76 Конфигурация .......................................................................................................................76 Состояние, связанное с содержимым............................................................................79 Состояние#документ............................................................................................................85 Окна.................................................................................................................................................86 Отображение окна................................................................................................................88 Задание размера и положения .........................................................................................91 Объекты Window и Application.......................................................................................92 Пользовательские элементы управления ........................................................................93 Навигация и страницы .............................................................................................................96 Передача состояния между страницами ....................................................................101 Управление навигацией...................................................................................................106 Управление журналом......................................................................................................107 Функциональная навигация и страничные функции...........................................109 Исполнение приложений в браузере...............................................................................114 HelloBrowser.........................................................................................................................115 Под капотом .........................................................................................................................119 Независимая разметка......................................................................................................121 Чего мы достигли?...................................................................................................................122 Глава 3. Элементы управления ..........................................................................................123 Принципиальные основы элементов управления.......................................................123 Модель содержимого ..............................................................................................................125 Элемент ContentPresenter.....................................................................................................126 Свойство Items ..........................................................................................................................128 Свойства Children и Child.....................................................................................................129 Шаблоны .....................................................................................................................................130 Привязка шаблона ...................................................................................................................134 Размышления о шаблонах.....................................................................................................136 Библиотека элементов управления ..................................................................................137 Кнопки ...................................................................................................................................137 Оглавление 9 Списки....................................................................................................................................139 Меню и панели инструментов.......................................................................................146 Контейнеры ..........................................................................................................................150 Средства просмотра документов ..................................................................................164 Строительные блоки .........................................................................................................167 Чего мы достигли?...................................................................................................................176 Глава 4. Размещение ..............................................................................................................177 Принципы размещения .........................................................................................................177 Контракт о размещении ...................................................................................................178 Согласованное размещение ............................................................................................180 Отсутствие встроенного размещения .........................................................................187 Библиотека менеджеров размещения ............................................................................188 Панель Canvas .....................................................................................................................188 Панель StackPanel..............................................................................................................191 Панель DockPanel ..............................................................................................................192 Панель WrapPanel .............................................................................................................195 Панель UniformGrid ..........................................................................................................196 Панель Grid ................................................................................................................................197 Концептуальная модель элемента Grid......................................................................198 Организация размещения в элементе Grid...............................................................203 Элемент GridSplitter .........................................................................................................206 Реализация нестандартного размещения......................................................................208 Чего мы достигли?...................................................................................................................214 Глава 5. Визуальные элементы ..........................................................................................215 Двумерная графика ................................................................................................................215 Принципы двумерной графики.....................................................................................216 Геометрические примитивы ...........................................................................................219 Цвет .........................................................................................................................................222 Кисти ......................................................................................................................................224 Перья.......................................................................................................................................231 Рисунки..................................................................................................................................235 Фигуры ..................................................................................................................................236 Изображения........................................................................................................................237 Прозрачность .......................................................................................................................244 Свойство BitmapEffects....................................................................................................247 Трехмерная графика ..............................................................................................................248 Программа «Здравствуй, мир» в трехмерной ипостаси.......................................249 Принципы трехмерной графики...................................................................................252 Документы и текст ..................................................................................................................260 «Здравствуй, мир» – текстовый вариант...................................................................260 10 Оглавление Шрифты.................................................................................................................................265 Размещение текста.............................................................................................................266 Дополнительные типографические средства...........................................................273 Анимация ....................................................................................................................................274 Анимация как new Timer.................................................................................................274 Время и временная шкала...............................................................................................282 Определение анимации....................................................................................................283 Интеграция анимации ......................................................................................................286 Мультимедиа .............................................................................................................................289 Аудио ......................................................................................................................................290 Видео.......................................................................................................................................292 Чего мы достигли?...................................................................................................................294 Глава 6. Данные........................................................................................................................295 Принципы работы с данными .............................................................................................295 Модель данных в .NET.....................................................................................................295 Всепроникающее связывание ........................................................................................296 Преобразование данных ..................................................................................................297 Ресурсы .......................................................................................................................................297 Основные принципы связывания......................................................................................302 Привязка к объектам CLR...................................................................................................308 Редактирование...................................................................................................................311 Привязка к XML ......................................................................................................................316 Знакомство с XPath...........................................................................................................316 Привязка к XML ................................................................................................................317 Шаблоны данных.....................................................................................................................321 Выбор шаблона ...................................................................................................................324 Более сложное связывание .................................................................................................326 Иерархическое связывание ............................................................................................326 Представления наборов ...................................................................................................331 Отображение, управляемое данными .............................................................................338 Чего мы достигли?...................................................................................................................344 Глава 7. Действия ....................................................................................................................345 Принципиальные основы действий ..................................................................................345 Композиция элементов ....................................................................................................345 Слабая связь.........................................................................................................................346 Декларативные действия.................................................................................................348 События .......................................................................................................................................348 Команды ......................................................................................................................................352 Оглавление 11 Команды и привязка к данным .....................................................................................356 Триггеры ......................................................................................................................................361 Добавление триггеров к данным...................................................................................361 Добавление триггеров к элементам управления.....................................................364 Триггеры как новый вариант if .....................................................................................365 Чего мы достигли?...................................................................................................................366 Глава 8. Стили ...........................................................................................................................367 Принципы стилизации ...........................................................................................................367 Композиция элементов ....................................................................................................367 Унифицированная модель настраивания..................................................................369 Оптимизация для инструментальных средств ........................................................371 Введение в стили......................................................................................................................372 Модели, отображение и стили.......................................................................................376 Темы........................................................................................................................................379 Обличья .......................................................................................................................................381 Наследование стилей ........................................................................................................386 Применение стилей не во зло .............................................................................................388 Создавайте темы, а не стили ..........................................................................................389 В единообразии сила.........................................................................................................390 Сформулируйте главную идею .....................................................................................391 Чего мы достигли?...................................................................................................................392 Приложение. Базовые службы..........................................................................................393 Потоки и диспетчеры .............................................................................................................393 Свойства ......................................................................................................................................398 Свойства .NET.....................................................................................................................398 Система свойств WPF......................................................................................................402 Метаданные ..........................................................................................................................404 Клавиатура, мышь и стилос ................................................................................................407 Класс InputBinding ............................................................................................................408 Взаимодействие с устройствами ввода ......................................................................410 Фокус клавиатуры .............................................................................................................411 Чего мы достигли?...................................................................................................................412 Предметный указатель ..........................................................................................................413 12 Предисловие Крис Селлс Слава Богу, что в те времена, когда я прозябал вне корпорации Microsoft, не было таких людей, как Крис Андерсон. Сейчас я работаю в Microsoft (через два кабинета от Криса, кстати), но не так давно вел курсы в компании, занимающейся обучением разработчиков для Windows. Мной и моими коллегами руководил глубокомысленный соискатель звания доктора философии, который на работе был так же суров, как и в своих ученых занятиях, подчиняясь принципу «растопчи или будешь растоптан», гос# подствующему в академических кругах. Мы научились ясно мыслить, поскольку это был защитный механизм, и ясно выражаться, так как это был способ выжива# ния. Если мы точно не следовали его стандартам, он отодвигал нас в сторону и пе# ределывал нашу работу прямо у нас на глазах (мы называли это «подметанием» и всячески стремились избежать подобного унижения). Точно так же, мы научились игнорировать учебные и справочные материалы, поставляемые тем или иным производителем, поскольку нам было очевидно, что как бы ясно авторы ни мыслили в собственных стенах, для общения с внешним миром это никуда не годится. Можно даже сказать, что почти десять лет мы зани# мались тем, что «подметали» за корпорацией Microsoft, представляя подготов# ленные ей материалы в виде коротких курсов, выступлений на конференциях, журнальных статей и книг. Мы называли это «Законом о непрерывном найме в Microsoft» и считали корпорацию источником финансирования, позволявшим нам вести жизнь, к которой мы привыкли. Вообще#то, мы неплохо жили, разъезжая по стране и произнося сентенции типа «не забывайте вызывать Release», «избегайте лишних обращений к сер# веру», «забудьте про агрегирование», поскольку это ясные инструкции для разработчиков, которые Microsoft почему#то не могла сформулировать самос# тоятельно. Я не хочу сказать, что внутри Microsoft нет людей с четким мыш# лением (я, к примеру, очень уважаю Тони Вилльямса (Tony Williams) и Крис# пина Госуэлла (Crispin Goswell)), но в те времена наблюдался зияющий раз# рыв между новичками и людьми, способными воспринять заумные техничес# кие тексты. С появлением этой книги нашему выгодному предприятию пришел конец. Крис Андерсон является одним из главных архитекторов нового поколения гра# фических интерфейсов – технологии Windows Presentation Foundation, которая и является темой настоящей книги. Если вы думаете, что сама суть работы архи# 13 тектора – позаботиться о том, чтобы все внутренние проблемы были правильно решены, и оставить другим навешивать «бантики», – делает Криса не вполне под# ходящим человеком для того, чтобы объяснить разработчикам, куда двигаться и где остановиться, то смею заверить, что это не так. Глубоко разбираясь во внут# реннем устройстве продукта, Крис сумел внятно рассказать всем желающим слу# шать о том, что лежит в основе его детища (к которому, не будем забывать, при# ложило руки еще более 300 человек). Будучи автором книги на ту же тему, вышедшей в другом издательстве, не мо# гу сказать, что это единственная книга о WPF, которая вам понадобится (иначе мне пришлось бы предстать перед расстрельной командой). Зато с уверенностью заявляю: она должна стоять на вашей полке и всегда быть под рукой. Именно так я намерен поступить со своим экземпляром. sellsbrothers.com Октябрь 2006 Предисловие Дон Бокс Ух, не могу поверить, что наконец#то .NET 3.0 и Windows Vista вышли в свет. Хорошо помню, как за кулисами конференции для профессиональных разра# ботчиков PDC 2003 Крис пытался подготовить живую демонстрацию .NET 3.0 (тогда он назывался WinFX) для основного докладчика Джима Олчина (Jim Allchin). Это было очень непросто, поскольку тогда горели леса под Лос#Андже# лесом и отменили рейс, на котором должен был лететь Крис. К счастью, на месте уже был Крис Селлс (Chris Sells), который был готов подменить Криса Андерсо# на на демонстрации, если бы тот не успел вовремя. В то время основная работа Андерсона в корпорации Microsoft заключалась в том, чтобы обеспечить оглуши# тельный успех Vista, включая и Windows Presentation Foundation (WPF). Кто мог знать, что до поставки продукта остается еще почти четыре года (конечно, та# кой длительный срок – непременное условие успеха). Так что же такого необычного в технологии WPF? Как и входящая в состав .NET 3.0 родственная технология Windows Workflow Foundation (WF), WPF – это подход к разработке программного обеспечения на базе принципа «всем миром». В нем применяется язык XAML, который позволя# ет участвовать в разработке людям с различными знаниями и опытом. В случае WWF на языке XAML формулируются описания процессов и правил, которые затем объединяются с императивным кодом, написанным на C# или Visual Basic. В случае же WPF язык XAML – это мост между нами, трудягами#программиста# ми, и теми творцами, которые носят береты и водолазки и создают визуальные образы, выглядящие так, будто не мы их спроектировали. WPF – это действительно впечатляющая технология. Она объединяет доку# менты, формы и мультимедийное содержание в пакет, содержащий код на языке разметки и языке программирования. 14 Предисловие Но что меня поражает, так это как Крис в свободное от работы время ухитрил# ся написать книгу, которую вы сейчас держите в руках, как он сумел превратить плоды четырехлетнего труда (включая и снимки с экрана!) в такой компактный и легко читаемый текст. За эти годы мне доводилось часто беседовать с Крисом о различных нюансах WPF – иногда по телефону, иногда в его кабинете (он как раз напротив моего), а иногда за покерным столом. Тем не менее из этой книги я узнал много нового. Теперь, когда выпущена окончательная версия продукта, можно включать прожектора! Ярроу Пойнт, штат Вашингтон Январь 2007 15 Вступление Последние девять лет я работал в корпорации Microsoft над многими проекта# ми, связанными с пользовательскими интерфейсами. Я участвовал в разработке Visual Basic 6.0; версии Windows Foundation Classes, вошедшей в состав Visual J++ 6.0; Windows Forms для .NET Framework; во внутренних проектах, которые не дошли до широкой аудитории и, наконец, в проекте Windows Presentation Foundation (WPF). Я приступил к работе над WPF примерно через 18 месяцев после формирова# ния команды – осенью 2002 года – в роли архитектора. В то время и до конца 2005 года проект назывался Avalon. В начале 2003 года мне была оказана честь поуча# ствовать в переработке проекта платформы, которая была представлена на конфе# ренции для профессиональных разработчиков PDC 2003 в Лос#Анджелесе в каче# стве предварительного анонса. Ряд концепций, нашедших место в WPF, появился еще в 1997 году (в наборе классов Application Foundation Classes для Java уже бы# ли заложены некоторые идеи, приведшие к созданию компонентов в WPF). Когда я присоединился к команде, проект еще находился на стадии исследо# вательских работ. Количество идей намного превышало то, что может уместить# ся в одной версии. Основная цель WPF – заменить всю существующую инфра# структуру создания приложений на стороне клиента новой интегрированной платформой, которая сочетала бы лучшие черты Win32 и Web, – была на удив# ление амбициозной. Размывались границы между пользовательским интерфей# сом, документами и мультимедийным содержимым. Со временем мы кое от чего отказались (с болью в сердце), добавили новые интересные возможности, выс# лушали кучу замечаний от клиентов, но никогда не упускали из виду поставлен# ную цель. Краткий экскурс в историю графических интерфейсов пользователя Графические интерфейсы пользователя (ГИП) появились в начале 1980#х го# дов в лаборатории Xerox PARC. С тех пор Microsoft, Apple и многие другие ком# пании создали разнообразные платформы для разработки приложений с графи# ческим интерфейсом. Первой такой платформой от Microsoft была система Windows 1.0, но она не получила широкого распространения, пока в 1990 году не вышла версия Windows 3.0. Модель программирования графических приложе# ний включала две основные динамические библиотеки: User и GDI. В 1991 году Microsoft выпустила продукт Visual Basic 1.0, построенный на базе User и GDI, но предлагающий намного более простую модель программирования. 16 Вступление Пользоваться моделью, принятой в Visual Basic (она получила внутреннее название Ruby1), было куда проще, чем непосредственно Windows API. Эта прос# тота разгневала разработчиков, считавших, что программирование должно быть трудным делом. Впрочем, ранним версиям Visual Basic было присуще много ог# раничений, поэтому при создании «серьезных» приложений применялись «го# лые» библиотеки User и GDI. Со временем все изменилось. К тому моменту, ког# да Microsoft перешла на 32#разрядную платформу – с выходом Windows 95 и Visual Basic 4.0, создатели VB уже развернулись вовсю и предлагали гораздо бо# лее широкий диапазон средств, доступных на данной платформе. Примерно в то же время на рынке наметился еще один крупный сдвиг: в сторо# ну Интернет. В корпорации Microsoft тогда уже велись работы по замене модели пользовательского интерфейса, реализованной в Visual Basic, другой, получившей внутреннее название Forms3. По разным причинам Microsoft приняла решение воспользоваться этой моделью в качестве основы для браузера. Движок был пере# именован в Trident и теперь поставляется в составе Windows как MSHTML.dll. С годами Trident превратился в механизм, тесно связанный с HTML и обладающий развитой поддержкой для размещения текста, разметки и сценариев. Тогда же возникла еще одна идея: управляемый код. Visual Basic (как и многие другие языки) уже давно исполнялся в управляемой среде, но появление языка Java, созданного компанией Sun Microsystems в 1994 году, впервые привлекло вни# мание множества разработчиков к концепции виртуальной машины. На протяже# нии последующих нескольких лет управляемый код занимал все большую долю рынка, и в 2002 году Microsoft выпустила собственную универсальную платформу для разработки управляемого кода: каркас .NET Framework. В него вошла и техно# логия Windows Forms – управляемый API для программирования на базе библио# тек User32 и GDI+ (преемник GDI32). Предполагалось, что Windows Forms заме# нит старый пакет Ruby для создания форм, который был частью Visual Basic. На рубеже нового тысячелетия у Microsoft было четыре платформы для раз# работки ГИП: User32/GDI32, Ruby, Trident и Windows Forms. Эти технологии были предназначены для решения различных классов задач, в их основе лежали разные модели программирования и у каждой был свой круг пользователей. Гра# фические системы также эволюционировали. В 1995 году Microsoft анонсирова# ла систему DirectX, которая расширяла возможности прямого доступа к аппара# туре. Но ни одна из четырех основных технологий разработки ГИП не содержала серьезной поддержки появившихся средств. Предстояло решить сложную задачу. Пользователям требовалось богатство современных видеоигр и телевидения. Мультимедиа, анимация и выразительная графика должны были присутствовать повсеместно. Необходима была также поддержка обогащенного текста, поскольку чуть ли не в каждом приложении нужно было отображать те или иные документы. Пользователи хотели получить в свое распоряжение широкий набор современных элементов управления для представления кнопок, деревьев, списков и текстовых редакторов. Все это приго# дилось бы даже в самых простых приложениях. 1 Это название не имеет никакого отношения к языку программирования Ruby. Принципы WPF 17 Четыре основные платформы в совокупности удовлетворяли большую часть потребностей клиентов, но они были оторваны друг от друга. Смешение платформ было делом трудным и подверженным ошибкам. Да и с чисто эгоис# тической точки зрения руководство Microsoft (назовем вещи своими именами: Билл Гейтс) устало платить зарплату четырем коллективам, разрабатываю# щим технологии, которые в значительной части перекрывали друг друга. В 2001 году в Microsoft была сформирована новая команда, перед которой была поставлена задача создать унифицированную презентационную платформу, которая в конечном итоге заменила бы User32/GDI32, Ruby, Trident и Windows Forms и при этом отвечала бы новым потребностям пользователей. В состав команды вошли в основном люди, занимавшиеся прежними платформа# ми, а их целью было разработать продукт, вобравший в себя лучшее из лучше# го и тем самым совершить качественный скачок. Так появилась команда Avalon. На конференции PDC 2003 Microsoft анонсировала новую технологию именно под этим именем, но позже оно было заменено на Windows Presentation Foundation. Принципы WPF На создание WPF ушло много времени, но на протяжении всего проекта неко# торые основополагающие принципы оставались неизменными. Создать платформу для развитой презентации В описаниях новой технологии слово «развитая» (rich), наверное, исполь# зуется чрезмерно часто. Но я не могу найти термина, который более точно вы# ражал бы принцип, положенный в основу WPF. Мы ставили перед собой цель создать нечто, вобравшее в себя средства из всех существующих презентаци# онных технологий и многое сверх того: начиная от таких простых вещей, как векторная графика, градиенты и растровые эффекты, и кончая трехмерной графикой, анимацией, мультимедийным содержимым и типографическими средствами. Другая немаловажная сторона этого принципа отражена в слове «платформа». Мы хотели создать не просто механизм воспроизведения разви# того содержимого, но платформу для разработки крупномасштабных прило# жений, которую к тому же можно было бы расширять заранее непредвиден# ными способами. Создать программируемую платформу На ранней стадии команда WPF решила, что для платформы понадобится мо# дель программирования, содержащая декларативную (язык разметки) и импера# тивную (код) составляющую. Оглядываясь по сторонам, мы уже тогда поняли, что разработчики приняли новую среду для управляемого исполнения кода. До# вольно быстро принцип программируемой платформы превратился в принцип модели управляемого программирования. Нашей целью было сделать управляе# мый код основой модели, а не надстроенным позже уровнем. 18 Вступление Создать декларативную платформу С точки зрения потребителей и разработчиков программного обеспечения представлялось очевидным, что индустрия движется в сторону все более и более декларативных моделей программирования. Мы понимали, что для успеха WPF нам понадобится развитая, непротиворечивая и полная модель программирова# ния, основанная на языке разметки. Обзор текущих тенденций показывал, что язык XML становится стандартом де факто для обмена данными, поэтому мы ре# шили взять его за основу и назвали созданный диалект XAML (Extensible Application Markup Language – расширяемый язык разметки приложений). Интегрировать пользовательский интерфейс, документы и мультимедиа Возможно, самая серьезная проблема, с которой сталкиваются клиенты, зани# мающиеся прикладным программированием, – это разбиение функциональности на изолированные островки. Одна платформа для построения пользовательских интерфейсов, другая – для подготовки документов и множество платформ для создания мультимедийных материалов (трехмерной или двумерной графики, ви# део, анимации и т.д.). Приступая к разработке новой презентационной системы, мы твердо поставили перед собой цель: интеграция пользовательского интерфей# са, документов и мультимедиа должна быть высшим приоритетом всей команды. Взять лучшее из Web и лучшее из Windows Тут нашей целью было отобрать лучшие средства, созданные за 20 лет развития Windows и 10 лет развития Web, и включить их в новую платформу. Web предлага# ет замечательно простую модель разметки, способ развертывания приложений и еди# ную их структуру, а также развитые средства обращения к серверу. Windows же дает развитую клиентскую модель, простую модель программирования, контроль над внешним обликом приложения и разнообразные сетевые службы. Сложность задачи состояла в том, чтобы стереть границу между приложениями для Web и Windows. Свести программистов и дизайнеров По мере того как приложения обретают развитые графические возможности, учи# тывая пожелания пользователей, в процесс разработки приходится вовлекать ранее непричастные к нему сообщества. Медийные компании (типографские, онлайновые, телевизионные и т.п.) уже давно знают, что для создания впечатляющего продукта не# обходимы разноплановые дизайнеры. А теперь такие же требования выдвигаются и в области разработки программ. Исторически сложилось, что инструментарий дизайне# ра не имеет ничего общего с процессом конструирования программного обеспечения. Дизайнеры пользовались такими программами, как Adobe Photoshop или Adobe Illustrator, чтобы создавать весьма выразительные графические материалы, но это лишь раздражало программистов, которые пытались придуманный дизайн реализо# вать. Поэтому мы стремились создать унифицированную систему, которая могла бы естественно поддерживать возможности, необходимые дизайнерам, и применять язык разметки (XAML) для бесшовной интеграции различных инструментов. Что собой представляет эта книга 19 Что собой представляет эта книга О технологии WPF уже написано и еще будет написано много книг. Когда я впер# вые задумался о том, чтобы написать книгу, мне хотелось, чтобы мой труд чем#то от# личался от всех остальных. Эта книга рассчитана на разработчиков приложений; она задумана как концептуальный справочник, охватывающий большую часть WPF. Я тщательно подбирал слова в предыдущем предложении. Эта книга о приложениях. Существует два типа программ: для взаимодей# ствия с людьми и для взаимодействия с другими программами. Под приложени# ем я понимаю программу, написанную главным образом для взаимодействия с человеком. По существу, вся технология WPF посвящена именно взаимодей# ствию с людьми. Эта книга для разработчиков. Я хотел представить взгляд на платформу с точ# ки зрения программирования. Сам я, прежде всего, разработчик и, выступая ар# хитектором WPF, всегда считал внешнего разработчика своим главным заказчи# ком. В этой книге в основном рассматриваются вопросы, интересные разработчи# кам приложений. Хотя разработчик элементов управления тоже найдет в ней не# мало полезного для себя, я не ставил себе целью написать пособие на эту тему. Эта книга о концепциях, а не просто описание API. Если вам нужен справоч# ник по API, обратитесь к Google или MSN либо поищите в документации на сай# те MSDN. Я же намерен выдвинуть на первый план абстрактные идеи, рассказать обо всех «как» и «почему», относящихся к проектированию платформы, и пока# зать, как различные платформенные API, работая совместно, помогают програм# мистам выйти на новый качественный уровень. Эта книга является справочником; она организована как последовательность тематических разделов, чтобы читателю было удобно возвращаться к какой#то теме или забегать вперед в поисках ответа на конкретный вопрос. Необязательно читать подряд от корки до корки. В этой книге рассмотрены многие аспекты WPF, но не все. Когда я приступал к работе над ней, Крис Селлс дал мне ценный совет: «Что опустить, так же важ# но, как что включить». WPF – очень объемная платформа, поэтому, чтобы предс# тавить картину в целом, я вынужден был опустить некоторые части. В этой кни# ге собрано то, что я считаю важнейшими вехами, от которых надо двигаться вглубь территории. Моя задача – снабдить вас картой основных концепций, на которой показано, как они соотносятся между собой, и объяснить мотивы тех или иных проектных решений. Надеюсь, что, прочитав эту книгу, мы станете глубже понимать, что та# кое WPF, и сумеете углубить свои знания самостоятельно. Предварительные условия Чтобы читать эту книгу, вы должны быть знакомы с .NET. Экспертом быть не# обязательно, но знать о том, что такое классы, методы и события, необходимо. Все примеры написаны на языке C#. Для платформы WPF можно программировать на любом .NET#совместимом языке, но сам я обычно пользуюсь C#. 20 Вступление Организация В книге восемь глав и приложение, состоящее из трех частей. Для рассказа об истории создания платформы WPF я хотел отвести как можно меньше глав. • Введение (глава 1) посвящено краткому знакомству с платформой. Здесь описываются взаимозависимости между семью основными компонентами WPF. Здесь же вкратце объясняется процедура создания приложений на базе WPF, показывается, как пользоваться инструментами, входящими в комплект SDK, и как искать нужные материалы в документации. • Приложения (глава 2). В этой главе описывается структура приложения, основанного на WPF, и перечисляются прикладные службы и объекты верхнего уровня. • Элементы управления (глава 3). Рассматриваются основные паттерны проектирования, применяемые в элементах управления WPF, и наиболее важные семейства элементов. Элементы управления – это базовые строи# тельные блоки, из которых в WPF конструируется пользовательский ин# терфейс. Если вы хотите ограничиться прочтением только одной главы, выберите эту. • Размещение (глава 4). Рассматривается устройство системы размещения, дает# ся обзор шести готовых менеджеров размещения, входящих в состав WPF. • Визуализация (глава 5). Приводится обзор очень широкой темы – системы визуализации в WPF. Сюда входят типографические средства, двумерная и трехмерная графика, анимация, видео и аудио. • Данные (глава 6). Здесь нашли отражение основные понятия об источни# ках данных, привязке к данным, ресурсах и операциях передачи данных. • Действия (глава 7). Обзор того, как с помощью событий, команд и тригге# ров приложение приходит в движение. • Стили (глава 8). Рассматривается система стилизации в WPF. Стили поз# воляют четко разграничить работу дизайнера и программиста за счет обес# печения слабой связи между программной структурой и визуальным представлением пользовательского интерфейса. • Приложение Базовые службы посвящено некоторым низкоуровневым службам WPF. Здесь рассматривается потоковая модель, система свойств и событий, ввод данных, композиция и печать. Благодарности Для меня эта книга стала серьезным предприятием. Раньше мне приходилось работать над статьями, презентациями и техническими документами, но никако# го предварительного опыта, необходимого для того, чтобы уложить описание та# кой обширной платформы, как WPF, в сравнительно небольшую книгу, у меня не было. Об авторе 21 Я посвятил эту книгу своей жене Меган. Она все время поддерживала меня и в этом проекте (даже когда я брал с собой ноутбук на выходные!), и во всех дру# гих начинаниях. Вся команда проекта Avalon оказала мне огромную помощь при работе над этой книгой (и самим продуктом!). В работе над проектом меня поддерживал мой менеджер Ян Эллисон#Тейлор (Ian Ellison#Taylor). Сэм Бент (Sam Bent), Джефф Богдан (Jeff Bogdan), Вайвек Далви (Vivek Dalvi), Намита Гупта (Namita Gupta), Майк Хиллберг (Mike Hillberg), Роберт Ингебретсен (Robert Ingebretsen) Дэвид Дженни (David Jenni), Лоран Лавуайе (Lauren Lavoie), Ашра Михаил (Ashraf Michail), Кэвин Мур (Kevin Moore), Грэг Шехтер (Greg Schechter) – членов ко# манды слишком много, всех не перечислишь. Мне доставляет огромное удоволь# ствие работать со всеми ними. Я благодарен Дону Боксу, побудившему меня взяться за эту книгу, и Крису Селлсу за мудрые советы, которые он давал несмот# ря даже на то, что мы работали над конкурирующими книгами. Моему редактору Майклу Вейнхардту (Michael Weinhardt) я обязан тем, что книга получилась качественной. Майкл читал, перечитывал, правил и снова пра# вил каждый ее раздел. Он не позволял мне соглашаться ни на что, кроме совер# шенства. Все ошибки и неудачные переходы целиком лежат на моей совести. Джоан Мюррей (Joan Murray), Карен Гетман (Karen Gettman), Джулия Нахил (Julie Nahil) и весь персонал издательства Addison#Wesley проделали вместе со мной колоссальную работу. Стефани Хиберт (Stephanie Hiebert), литературный редактор, потратив бесчисленные часы на правку правописания, грамматики и стиля, превратила мои бессвязные заметки в настоящий английский текст. И, наконец, я благодарен рецензентам. Эрик Эллис (Erick Ellis), Джо Флэ# ниган (Joe Flanigan), Джессика Фослер (Jessica Fosler), Кристоф Назар (Christophe Nasarre), Ник Палдино (Nick Paldino), Крис Селлс (Chris Sells) и многие другие высказали немало ценных замечаний. От Джессики я получил такую глубокую и конструктивную критику, какую мне никогда не доводилось читать раньше. Уверен, что многих я забыл упомянуть, за что приношу свои из# винения. Крис Андерсон Ноябрь 2006 simplegeek.com Об авторе Крис Андерсон работает архитектором программного обеспечения в подразде# лении взаимосвязанных систем в корпорации Microsoft. Его основная обязан# ность – проектирование архитектуры основанных на каркасе .NET технологий, применяемых для реализации приложений и служб следующего поколения. За десять лет работы в Microsoft Крис принимал участие в разработке Visual Basic 6.0, Visual J++ 6.0, .NET Framework версий 1.0 и 1.1, а в последнее время Windows Presentation Foundation. Крис преимущественно занимался презентационными технологиями: созданием элементов управления для Visual Basic, разработкой 22 Вступление библиотеки классов Windows Foundation Classes для Visual J++, а также работой над Windows Forms и ASP.NET в составе .NET Framework. В 2002 году Крис вошел в команду проекта Windows Client в должности веду# щего архитектора Windows Presentation Foundation. Благодаря своим знаниям и опыту, Крис выступал с презентациями и был основным докладчиком на много# численных конференциях (PDC, Tech#Ed, Win#Dev, DevCon и других). Крису принадлежит множество печатных и онлайновых публикаций. Среди других увлечений Криса можно упомянуть цифровую фотографию, ве# дение собственного онлайнового журнала (блога), видеоигры, дайвинг, гонки на катерах и домашний кинотеатр. Со всем этим его жена, Меган, терпеливо мирится. Это первая книга Криса Андерсона. 23 Глава 1. Введение Windows Presentation Foundation (WPF) – это большой шаг вперед в техно# логиях создания пользовательских интерфейсов. В этой главе мы поговорим об основных принципах WPF и дадим краткий обзор платформы в целом. Можете считать, что эта глава предваряет материал, изложенный далее. WPF как новый ГИП Прежде чем обратиться собственно к WPF, интересно было бы вспомнить, с чего мы начинали. Библиотека User32 в стиле книги Чарльза Петцольда Всякий, кто программировал на уровне User32, наверняка читал какую#то из книг Чарльза Петцольда «Программирование в Windows». Все они начинаются примерно с такого примера: #include <windows.h> LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR cmdline, int cmdshow) { MSG msg; HWND hwnd; WNDCLASSEX wndclass = { 0 }; wndclass.cbSize = sizeof(WNDCLASSEX); wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = WndProc; wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wndclass.lpszClassName = TEXT(«Window1»); wndclass.hInstance = hInstance; wndclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION); RegisterClassEx(&wndclass); hwnd = CreateWindow(TEXT(«Window1»), TEXT(«Hello World»), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, Глава 1. Введение 24 CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); if( !hwnd ) return 0; ShowWindow(hwnd, SW_SHOWNORMAL); UpdateWindow(hwnd); while( GetMessage(&msg, NULL, 0, 0) ) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { switch(msg) { case WM_DESTROY: PostQuitMessage(WM_QUIT); break; default: return DefWindowProc(hwnd, msg, wparam, lparam); } return 0; } Это вариант программы «Здравствуй, мир» на языке User32. В ней происхо# дит немало интересного. Сначала путем обращения в функции RegisterClassEx определяется специализированный тип (Window1), затем создается (Create Window) и отображается (ShowWindow) экземпляр этого типа. В конце запуска# ется цикл обработки сообщений, в котором окно получает данные от пользовате# ля и события от системы (GetMessage, TranslateMessage и Dispatch Message). Эта программа не претерпела существенных изменений со времен появления библи# отеки User в системе Windows 1.0. На основе этой сложной модели в Windows Forms была создана ясная управ# ляемая объектная модель, для которой программировать гораздо проще. Так, программа «Здравствуй, мир» в Windows Forms занимает всего 10 строк: using System.Windows.Forms; using System; class Program { [STAThread] static void Main() { Form f = new Form(); f.Text = «Hello World»; WPF как новый ГИП 25 Application.Run(f); } } Основная цель WPF – по возможности не отбрасывать те знания, которые уже на# копили разработчики. Хотя как презентационная система WPF очень сильно отлича# ется кардинально от Windows Forms, но эквивалентная программа оказывается очень похожа1 (отличающиеся строки в коде ниже выделены полужирным шрифтом): using System.Windows; using System; class Program { [STAThread] static void Main() { Window f = new Window(); f.Title = «Hello World»; new Application().Run(f); } } В обоих случаях вызов метода Run объекта Application служит заменой циклу обработки сообщений, а для определения экземпляров и типов применяется стандартная система типов CLR (общеязыковой среды исполнения). Windows Forms – это, по существу, управляемый слой поверх User32, а потому он ограни# чен базовыми возможностями, предоставляемыми этой библиотекой. User32 – великолепная платформа для создания двумерных элементов управ# ления. Она основана на системе рисования по запросу, с отсечением. Иными сло# вами, когда нужно нарисовать элемент, система обращается к пользовательскому коду (посылает запрос) с требованием выполнить рисование в охватывающем прямоугольнике, который сама защищает (реализует отсечение). Особенностью систем рисования с отсечением является высокое быстродействие; на буфериза# цию содержимого элемента не расходуется память, а на рисование чего#либо кро# ме самого изменившегося элемента не тратится процессорное время. Минусы таких систем связаны, главным образом, со временем реакции и ком# позицией. Поскольку система должна просить пользовательскую программу вы# полнять все операции рисования, то, пока рисуется один компонент, остальным приходится ждать. Эта проблема наглядно проявляется в Windows, когда прило# жение зависает, и его окно становится белым, или когда оно рисует некорректно. Кроме того, очень трудно добиться того, чтобы на один и тот же пиксель воздей# ствовали два компонента, а между тем такая возможность часто требуется – нап# ример, для реализации полупрозрачности, сглаживания (anti#aliasing) и теней. Недостатки Windows Forms становятся очевидными, когда несколько элемен# тов управления перекрываются (рис. 1.1). В этом случае система должна выпол# нить отсечение для каждого элемента. Обратите внимание на серую область вок# руг слова linkLabel1 на рисунке. 1 По мере усложнения программ различия между WPF и Windows Forms станут более ощутимы. 26 Глава 1. Введение Рис. 1.1. Перекрывающиеся элементы управления Windows Forms. Обратите внимание, как каждый элемент загораживает другие. В WPF композиция основана на запоминании содержимого. Для каждого компо# нента хранится список команд рисования. Это позволяет системе самостоятельно ри# совать содержимое элемента, не взаимодействуя с пользовательским кодом. Кроме то# го, реализованный алгоритм рисования гарантирует, что перекрывающиеся элементы рисуются, начиная с самого нижнего, то есть следующий рисуется поверх предыду# щих. Тем самым система управляет графическим ресурсом аналогично тому, как CLR управляет памятью. Это дает возможность добиться весьма полезных результатов. Система может производить высокоскоростную анимацию, посылать команды рисо# вания другому компьютеру и даже проецировать изображение на дисплее на трехмер# ные поверхности – при этом сам элемент ничего не знает обо всех этих сложностях. Для иллюстрации описанных эффектов сравните рисунки 1.1 и 1.2. На рис. 1.2 для всех элементов управления WPF задана частичная прозрачность, даже отно# сительного фонового изображения. Система композиции в WPF реализована на базе векторной графики, то есть процесс рисования сводится к проведению последовательности линий. На рис. 1.3 векторная графика сравнивается с традиционной растровой. Система также поддерживает полный спектр геометрических преобразова# ний: масштабирование, вращения и перекос. Как видно на рис. 1.4, к любому эле# менту можно применить любое преобразование, что приводит к своеобразным эффектам, хотя функциональность элемента при этом не теряется. Заметим, что во времена разработки библиотек User32 и GDI32 не существо# вало концепции вложенности контейнеров. Предполагалось, что в одном роди# тельском окне располагается плоский список потомков. Для простых диалоговых окон 1990#х годов этого хватало, но для нынешних сложных пользовательских интерфейсов требуется вложенность. Простейший пример этой проблемы возни# кает в связи с рассмотрением элемента GroupBox (группирующая рамка). В User32 GroupBox находится позади элементов управления, но не содержит их. Windows Forms поддерживает вложенность, но реализация этой возможности об# нажила многочисленные проблемы в модели управления, присущей User32. WPF как новый ГИП 27 Рис. 1.2. WPF поддерживает перекрытие за счет задания коэффициента прозрачности. Обратите внимание, что все элементы видны целиком, не исключая и фонового изображения. Оригинал Увеличенный растр Увеличенный вектор Рис. 1.3. Сравнение векторной и растровой графики. Обратите внимание, что масштабирование векторной линии не увеличивает зубчатость. 28 Глава 1. Введение Рис. 1.4. Вид элементов управления WPF в результате применения различных преобразований. Несмотря на искаженный вид, все они в полной мере сохраняют функциональность. Рис. 1.5. Элементы управления WPF являются вложенными и составными. Показанная кнопка содержит текст и рисунок. В ядре WPF все элементы управления являются контейнерными, группиру# ющими и составными. Так, кнопка в WPF на самом деле состоит из нескольких меньших элементов. Этот шаг в сторону поддержки композиции в сочетании с векторной графикой позволяет поддержать вложенность произвольного уровня (рис. 1.5). Чтобы лучше прочувствовать мощь механизма композиции, взгляни# те на рис 1.6. На исходной кнопке круг, содержащий картинку, занимает меньше пикселя. На этой кнопке располагается векторное изображение, содержащее полный текст документа, в котором есть еще одна кнопка, а на ней другое изоб# ражение. Помимо преодоления ограничений, присущих User32 и GDI32, одной из це# лей WPF было привнесение в модель, знакомую программистам Windows, луч# ших черт модели программирования для Web. HTML или, подругому, Web 29 Рис. 1.6. Вся мощь композиции раскрывается при увеличении кнопки. HTML или, по)другому, Web Один из наиболее существенных плюсов разработки для Web – простота соз# дания контента. Простейшая «программа» на языке HTML – это не более чем несколько тегов внутри текстового файла: <html> <head> <title>Hello World</title> </head> <body> <p>Welcome to my document!</p> </body> </html> Можно вообще опустить все теги и создать просто файл, содержащий текст «Welcome to my document!», назвать его <something>.html и просмотреть в брау# зере (рис. 1.7). Такие чрезвычайно низкие требования к уровню знаний привели к тому, что разработчиками стали миллионы людей, даже не помышлявших о программировании. В WPF можно добиться того же за счет использования нового языка раз# метки XAML (Extensible Application Markup Language – расширяемый язык разметки приложений), произносится «заммль». Поскольку XAML – это диа# лект XML, его синтаксис чуть более строгий. Пожалуй, самое очевидное требо# вание — необходимость ассоциировать директиву xmlns с пространством имен каждого тега: <FlowDocument xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’> <Paragraph>Welcome to my document!</Paragraph> </FlowDocument> Глава 1. Введение 30 Рис. 1.7. Отображение простого HTML)документа в браузере Internet Explorer Рис. 1.8. Отображение WPF)документа в браузере Internet Explorer Этот файл можно просмотреть, дважды щелкнув по имени <something>.xaml (рис. 1.8). Этот простой язык разметки позволяет задействовать всю мощь WPF. Совсем несложно описать на нем кнопку, изображенную на рис. 1.5, и отобразить ее в браузере (рис. 1.9). Одно из серьезных ограничений модели на базе HTML заключается в том, что этот язык предназначен для создания приложений, работающих внутри браузера. Напротив, разметка на языке XAML может быть не только отображена в браузе# ре (это мы уже видели), но и скомпилирована в стандартное Windows#приложе# ние (рис. 1.10). HTML или, подругому, Web 31 Рис. 1.9. Отображение WPF)документа в браузере Internet Explorer с применением элементов управления и менеджеров размещения, имеющихся в WPF Рис. 1.10. Запуск приложения, написанного на языке XAML. Эту программу можно выполнить в окне верхнего уровня или в браузере <Window xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ Title=’Hello World!’> <Button>Hello World!</Button> </Window> В HTML можно программировать на трех уровнях: декларативно, с помощью сценариев и на стороне сервера. Декларативное программирование многие вооб# ще за программирование не считают. В HTML с помощью простого тега <form/> можно объявить поведение, которое позволит выполнять действия (в общем слу# чае отправить данные серверу). Язык сценариев JavaScript позволяет писать программы в терминах объектной модели документа (DOM). Сценарии стано# вятся все более популярными, поскольку в настоящее время существует доста# 32 Глава 1. Введение точно браузеров, поддерживающих единую модель написания сценариев, так что один и тот же сценарий будет работать повсюду. На стороне сервера можно зап# рограммировать логику взаимодействия сервера с пользователем (на платформе Microsoft для этого применяется технология ASP.NET). В ASP.NET реализован очень изящный способ генерирования HTML#контен# та. С помощью повторителей, привязки к данным и обработчиков событий мож# но написать простое приложение. Один из самых тривиальных примеров – встав# ка фрагментов кода в разметку: <%@ Page %> <html> <body> <p><%=DateTime.Now().ToString()%></p> </body> </html> Истинная мощь ASP.NET проистекает из наличия богатой библиотеки сер# верных элементов управления и служб. С помощью одного лишь элемента DataGrid можно генерировать самый разнообразный HTML#контент, а, пользу# ясь такими службами, как управление членством, можно без труда создавать сай# ты, поддерживающие аутентификацию. Но у этой модели есть существенное ограничение – необходимость иметь подключение к сети. Современные приложения часто работают автономно или с эпизодическими обращениями к серверу. WPF позаимствовала у ASP.NET мно# гие средства, в частности, повторители и привязку к данным, но при этом дает программистам, работающим в среде Windows, возможность писать автономные программы. Одна из главных целей WPF – объединить лучшее из программирования для Windows и Web. Прежде чем знакомиться со средствами WPF, важно разобрать# ся в новой модели программирования, которая заложена в .NET Framework 3.0: языке XAML. Краткое знакомство с моделью программирования XAML Одна из основных и часто неправильно понимаемых особенностей каркаса .NET 3.0 – новая модель программирования XAML. Язык XAML обладает допол# нительной по сравнению с XML семантикой, которая допускает единую интерп# ретацию. Слегка упрощая, можно сказать, что XAML – это основанный на XML сценарный язык для создания объектов CLR. Имеется соответствие межу XML# тегами и типами CLR, а также между XML#атрибутами и свойствами и события# ми CLR. В следующем примере показано, как создать объект и присвоить значе# ние его свойству на языках XAML и C#: <!— версия на XAML —> <MyObject Краткое знакомство с моделью программирования XAML 33 SomeProperty=’1’ /> // версия на C# MyObject obj = new MyObject(); obj.SomeProperty = 1; XML#теги всегда определяются в контексте некоторого пространства имен, которое и описывает, какие теги являются допустимыми. В XAML пространства имен в смысле XML отображаются на наборы пространств имен и сборок в смыс# ле CLR. Чтобы приведенный выше простой пример заработал, необходимо уста# новить соответствие между требуемыми пространствами имен. В XML для опре# деления новых пространств имен применяется атрибут xmlns: <!— версия для XAML —> <MyObject xmlns=’clr namespace:Samples’ SomeProperty=’1’ /> // версия для C# using Samples; MyObject obj = new MyObject(); obj.SomeProperty = 1; В C# перечень сборок, в которых находятся используемые типы, всегда зада# ется в файле проекта или с помощью аргументов командной строки для запуска компилятора csc.exe. В XAML можно определить местоположение исходной сборки для каждого пространства имен: <!— версия для XAML —> <MyObject xmlns=’clr namespace:Samples;assembly=samples.dll’ SomeProperty=’1’ /> // версия для C# csc /r:samples.dll test.cs using Samples; MyObject obj = new MyObject(); obj.SomeProperty = 1; В XML мир разбит на две половины: элементы и атрибуты. Модель XAML бо# лее тесно связана с CLR, поскольку апеллирует к объектам, свойствам и событи# ям. Значения свойств можно представлять в виде атрибутов или дочерних эле# ментов. Предыдущий пример допустимо записать и так: <MyObject xmlns=’clr namespace:Samples;assembly=samples.dll’> <MyObject.SomeProperty> 34 Глава 1. Введение 1 </MyObject.SomeProperty> </MyObject> Каждый элемент, соответствующий свойству, квалифицируется типом, кото# рому это свойство принадлежит. Предположим, например, что есть еще одно свойство, значением которого является объект типа Person со свойствами FirstName и LastName. На XAML можно было бы легко выразить это соотноше# ние, воспользовавшись элементами для описания свойств: <MyObject xmlns=’clr namespace:Samples;assembly=samples.dll’> <MyObject.Owner> <Person FirstName=’Chris’ LastName=’Anderson’ /> </MyObject.Owner> </MyObject> XAML проектировался как язык разметки, тесно интегрированный с CLR и обеспеченный развитой инструментальной поддержкой. Дополнительно стави# лась цель создать такой формат, который было бы легко читать и записывать. Может показаться, что проектировать свойство платформы, которое оптимизи# ровано прежде всего для инструментов, а лишь потом для людей, не слишком вежливо, но команда WPF полагала, что приложения для WPF как правило бу# дут создавать с помощью таких программ визуального конструирования, как Microsoft Visual Studio или Microsoft Expression. Чтобы граница между инстру# ментами и людьми не была непроходимой, WPF позволяет автору типу опреде# лить одно свойство как контентное.2 В примере выше, если сделать свойство Owner типа MyObject контентным3, то в разметке можно будет опустить тег элемента, соответствующего этому свой# ству: <MyObject xmlns=’clr namespace:Samples;assembly=samples.dll’> <Person FirstName=’Megan’ LastName=’Anderson’ /> </MyObject> Чтобы воспринимать текст было еще удобнее, в XAML есть возможность расширения разметки. Это общий способ расширить синтаксический анализа# тор языка с целью создания более простой разметки. Расширения реализуют# ся в виде типов CLR и работают почти так же, как атрибуты CLR. Они заклю# чаются в фигурные скобки { }. Например, чтобы присвоить свойству специаль# ное значение null, можно воспользоваться встроенным расширением разметки Null: Это аналог «свойства по умолчанию» в Visual Basic. Для этого следует добавить атрибут System.Windows.Markup.ContentPropertyAttribute в определение типа. 2 3 Краткое знакомство с моделью программирования XAML 35 <MyObject xmlns=’clr namespace:Samples;assembly=samples.dll’> <Person FirstName=’Megan’ LastName=’{x:Null}’ /> </MyObject> В таблице 1.1 перечислены все встроенные расширения XAML. Таблица 1.1. Встроенные расширения XAML Пространство имен XAML Назначение Пример x:Array Создает массив CLR <x:Array Type=’{x:Type Button}’> <Button /> <Button /> </x:Array> x:Class Задает имя определяемого типа (используется только при компиляции разметки) <Window x:Class=’MyNamespace.MyClass ’>... </Window> X:ClassModifier <Window x:Class=’...’ Задает модификаторы x:ClassModifier=’Public’> определяемого типа ... («public» «internal» и т.д.) (используется только при </Window> компиляции разметки) x:Code <Window x:Class=’...’> Ограничивает блок <x:Code> встроенного кода public void DoSomething() (используется только при { компиляции разметки) ... } </x:Code> ... </Window> x:Key <Button> Задает ключ элемента <Button.Resources> (поддерживается только <Style для элементов, x:Key=’Hi’>...</Style> содержащихся в словарях) </Button.Resources> </Button> Глава 1. Введение 36 x:Name Задает имя элемента для <<sys:Int32 xmlns:sys=’clr namespace: ссылки на него в System;...’ программе (обычно x:Name=’_myIntegerValue’> используется, когда у элемента нет встроенного 5</sys:Int32> свойства name) x:Null Создает значение null. <Button Content=’{x:Null}’ /> x:Static Создает значение путем доступа к статическому полю или свойству типа. <Button Command=’{x:Static ApplicationCommands.Close}’ /> x:Subclass Предоставляет базовый тип для компиляции разметки на язык, в котором не поддерживаются частичные типы. x:Type Предоставляет тип CLR (эквивалент Type.GetType). x:TypeArguments Задает обобщенные аргументы типа для создания экземпляра обобщенного типа. x:TypeArguments =’{x:Type Button}’ /> x:XData <ControlTemplate TargetType=’{x:Type Button}’> ... </ControlTemplate> <gc:List xmlns:gc=’clrnamespace: System.Collections. Generic;...’ <XmlDataSource> Ограничивает блок встроенного XML; может <x:XData> <Book xmlns=’’ Title=’...’ использоваться только /> для свойств типа </x:XData> IXmlSerializable. </XmlDataSource> Расширения разметки ищутся точно так же, как теги объектов, то есть необходи# мо объявить XML#префикс «x», иначе синтаксический анализатор выдаст ошибку. В языке XAML определено специальное пространство имен для встроенных типов: <MyObject xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ xmlns=’clr namespace:Samples;assembly=samples.dll’> <Person FirstName=’Megan’ LastName=’{x:Null}’ /> </MyObject> Краткое знакомство с моделью программирования XAML 37 Кроме того, для любой сборки CLR (или набора таких сборок) можно опреде# лить имя, построенное по принципу URI и соответствующее пространствам имен и сборок CLR. Это можно считать эквивалентом старого доброго предложения #include ‘windows.h’, которое хорошо известно программистам на C/C++. В сбор# ках WPF этот механизм применяется, поэтому для импорта WPF в XAML#файл можно использовать любой формат: <!— вариант 1: импорт по пространству имен CLR —> <Window xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ xmlns= ‘clr namespace:System.Windows;assembly=presentationframework.dll’> </Window> <!— вариант 2: импорт по URI —> <Window xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’> </Window> Метод с применением синтаксиса URI хорош тем, что импортируются сразу несколько пространств имен и сборок CLR, а, значит, разметка получается более компактной и работать с ней проще. И последнее, что я хотел сказать о XAML, – это возможность расширять типы за счет свойств, предоставляемых другими типами. Такие присоединенные свой# ства – это просто безопасный относительно типов вариант добавленных свойств (expando properties) в языке JavaScript. В версии XAML, предназначенной для WPF, присоединенные свойства работают только, если и тип, в котором свойство определено, и тип, к которому оно присоединяется, наследуют классу DependencyObject. Однако в общей спецификации XAML такого требования нет. В следующем примере свойство Dock определено в типе DockPanel. Присое# диненному свойству всегда предшествует имя предоставляющего его типа, даже если такое свойство употребляется в качестве атрибута: <Window xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’> <DockPanel> <Button DockPanel.Dock=’Top’>Top</Button> <Button> <DockPanel.Dock>Left</DockPanel.Dock> Left </Button> <Button>Fill</Button> </DockPanel> </Window> 38 Глава 1. Введение XAML – довольно простой язык, в нем не очень много правил. В версии, ко# торая поставляется в составе .NET Framework 3.0, все определения тегов XAML реализованы в виде типов CLR. Поэтому все, что можно сделать с помощью раз# метки, можно написать и в виде компилируемой программы. В этой книге я буду иногда пользоваться разметкой4, а иногда кодом в зависимости от того, как про# ще проиллюстрировать ту или иную идею. Ну а теперь, познакомившись с основами XAML, перейдем к рассмотрению основных составных частей самой технологии WPF. Обзор WPF Приступая к работе над этой книгой, я хотел сделать ее настолько краткой, насколь# ко возможно, но не более (надеюсь, Эйнштейн извинит меня за парафраз его слов). Но, оставаясь верным этой установке, я все же хочу предложить читателю краткий обзор платформы в целом, чтобы заложить основы для понимания базовых концепций. С чего начать Есть много способов начать разговор о WPF: с браузера, с разметки или с кода. Но я уже так долго занимаюсь программированием, что не могу отказать себе в удовольствии начать с простой программы на C#. Любое WPF#приложение начи# нается с создания объекта Application, который управляет временем жизни прило# жения и отвечает за доставку работающей программе событий и сообщений. Как правило, программа хочет что#то показать пользователю. В WPF для это# го необходимо создать окно5. Мы уже знакомились с исходным текстом простей# шего WPF#приложения, так что следующий код не станет для вас сюрпризом: using System.Windows; using System; class Program { [STAThread] static void Main() { Application app = new Application(); Window w = new Window(); w.Title = «Hello World»; app.Run(w); } } Чтобы этот код откомпилировать, понадобится компилятор C#. Вызвать его можно двумя способами. Во#первых, запустить прямо из командной строки. Для компоновки с WPF необходимо указать ссылки на три сборки. Инструменты для построения WPF#приложений могут оказаться в разных местах в зависимости от способа установки. В примере ниже показано, как откомпилировать программу, если был установлен комплект .NET Framework 3.0 SDK, и мы запускаем компи# лятор из входящего в его состав окна компоновки: 4 В последующих главах я буду опускать в примерах разметки пространства имен «…/xaml/pres) entation» и «…/xaml». Я буду последовательно считать пространство имен presentation простран) ством имен по умолчанию, а префикс «x» зарезервирую для пространства имен XAML. 5 Позже мы увидим, что даже в приложениях с постраничной навигацией окно создается. Обзор WPF 39 csc /r:»%ReferenceAssemblies%»\WindowsBase.dll /r:»%ReferenceAssemblies%»\PresentationCore.dll /r:»%ReferenceAssemblies%»\PresentationFramework.dll /t:winexe /out:bin\debug\tour.exe program.cs При наличии одного исходного файла и двух#трех внешних сборок запускать компилятор таким образом достаточно удобно. Но лучше всего воспользоваться новой системой построения, которая включена в .NET Framework 3.0 SDK и Visual Studio 2005: MSBuild. Создать файл проекта для MSBuild сравнительно несложно. Вот как приведенная выше команда преобразуется в файл проекта: <Project DefaultTargets=’Build’ xmlns=’http://schemas.microsoft.com/developer/msbuild/2003’> <PropertyGroup> <Configuration>Debug</Configuration> <Platform>AnyCPU</Platform> <RootNamespace>Tour</RootNamespace> <AssemblyName>Tour</AssemblyName> <OutputType>winexe</OutputType> <OutputPath>.\bin\Debug\</OutputPath> </PropertyGroup> <ItemGroup> <Reference <Reference <Reference <Reference </ItemGroup> Include=’System’ /> Include=’WindowsBase’ /> Include=’PresentationCore’ /> Include=’PresentationFramework’ /> <ItemGroup> <Compile Include=’program.cs’ /> </ItemGroup> <Import Project=’$(MSBuildBinPath)\Microsoft.CSharp.targets’ /> <Import Project=’$(MSBuildBinPath)\Microsoft.WinFX.targets’ /> </Project> Чтобы откомпилировать приложение, можно вызывать MSBuild из команд# ной строки: msbuild tour.csproj При запуске этого приложения появится окно, изображенное на рис. 1.11. До# бившись этого результата, можно подумать и о чем#нибудь более интересном. Одно из самых важных (по крайней мере, с точки зрения разработчиков) види# Глава 1. Введение 40 мых изменений в WPF – это глубокая укорененность разметки в платформе. На языке XAML создавать приложения обычно намного проще. Переходим к разметке Для написания той же самой программы на языке разметки мы начнем с опре# деления объекта Application. Создадим новый файл App.xaml и введем в него та# кой текст: <Application xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ /> Рис. 1.11. Пустое окно приложения Запускать эту программу не очень интересно. С помощью свойства Main Window объекта Application можно определить окно: <Application xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’> <Application.MainWindow> <Window Title=’Hello World’ Visibility=’Visible’ /> </Application.MainWindow> </Application> Чтобы откомпилировать этот код, придется включить в проект определение приложения: <Project ...> ... <ItemGroup> <ApplicationDefinition Include=’app.xaml’ /> </ItemGroup> ... </Project> Обзор WPF 41 При попытке собрать этот проект мы получим сообщение об ошибке, так как, включив определение приложения, мы автоматически определили функцию «Main», которая конфликтует с одноименной функцией в файле program.cs. Поэто# му удалим этот файл из проекта, оставив только определение приложения. Теперь, запустив приложение, мы получим точно такой же результат, как на рис. 1.11. Обычно окно описывают не внутри определения приложения, а в отдельном XAML#файле. Перенесем определение окна в файл MyWindow.xaml: <Window xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ Title=’Hello World’ > </Window> А в определение приложения включим ссылку на этот файл: <Application xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ StartupUri=’MyWindow.xaml’ /> И, наконец, добавим файл с определением окна в проект. Для любой компи# лируемой разметки (за исключением определения приложения) применяется тип построения Page: <Project ...> ... <ItemGroup> <Page Include=’mywindow.xaml’ /> <ApplicationDefinition Include=’app.xaml’ /> </ItemGroup> ... </Project> Теперь, когда эталонная программа написана, работает и правильно организо# вана, мы можем на ее примере приступить к исследованию WPF. Основы Приложения для WPF состоят из ряда скомпонованных элементов управле# ния. Одним из них является уже встречавшийся нам объект Window. В качестве другого примера приведем всем хорошо известный элемент Button: <Window xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ Title=’Hello World’ > <Button>Howdy!</Button> </Window> Глава 1. Введение 42 При запуске этой программы появится окно, изображенное на рис. 1.12. Сразу отметим, что кнопка автоматически заняла все свободное место в окне. При изменении размеров окна кнопка по#прежнему будет занимать всю его площадь. Рис. 1.12. Простая кнопка в окне С каждым элементом управления в WPF связан определенный менеджер раз# мещения. В случае окна размещение задано так, что единственный дочерний эле# мент заполняет окно целиком. Чтобы поместить в окно более одного элемента, необходим контейнер. Наиболее распространенным контейнером в WPF являет# ся панель размещения. Она может иметь сколько угодно дочерних элементов, к которым применяет ту или иную политику размещения. Пожалуй, простейшим менеджером размещения является StackPanel (стопка): <Window xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ Title=’Hello World’ > <StackPanel> <Button>Howdy!</Button> <Button>A second button</Button> </StackPanel> </Window> Элемент StackPanel располагает дочерние элементы друг под другом (как по# казано на рис. 1.13). В WPF имеется еще множество элементов управления и менеджеров разме# щения (и, разумеется, вы можете создавать свои собственные). Чтобы познако# миться еще с несколькими, добавим их в разметку: Обзор WPF 43 Рис. 1.13. Две кнопки внутри StackPanel <Window xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ Title=’Hello World’ > <StackPanel> <Button>Howdy!</Button> <Button>A second button</Button> <TextBox>An editable text box</TextBox> <CheckBox>A check box</CheckBox> <Slider Width=’75’ Minimum=’0’ Maximum=’100’ Value=’50’ /> </StackPanel> </Window> Запустив эту программу, вы убедитесь, что со всеми элементами можно взаи# модействовать (рис. 1.14). Теперь заменим менеджер StackPanel на какой#нибудь другой, например, WrapPanel: <Window xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ Title=’Hello World’ > <WrapPanel> <Button>Howdy!</Button> <Button>A second button</Button> <TextBox>An editable text box</TextBox> 44 Глава 1. Введение <CheckBox>A check box</CheckBox> <Slider Width=’75’ Minimum=’0’ Maximum=’100’ Value=’50’ /> </WrapPanel> </Window> Рис. 1.14. Еще несколько элементов управления в окне Рис. 1.15. Несколько элементов управления внутри WrapPanel Запустив новый вариант программы, мы увидим, что элементы размещаются совсем по#другому (рис. 1.15). Поглядев на несколько элементов управления, давайте напишем код, который позволит с ними взаимодействовать. Чтобы ассоциировать файл разметки с кодом, нужно выполнить несколько шагов. Сначала укажем в файле разметки имя класса: <Window x:Class=’EssentialWPF.MyWindow’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ Обзор WPF 45 Title=’Hello World’ > <WrapPanel> <Button>Howdy!</Button> <Button>A second button</Button> <TextBox>An editable text box</TextBox> <CheckBox>A check box </CheckBox> <Slider Width=’75’ Minimum=’0’ Maximum=’100’ Value=’50’ /> </WrapPanel> </Window> Для ассоциирования дополнительного кода с разметкой очень часто применяются появившиеся в версии C# 2.0 частичные типы. Чтобы определить файл с кодом, нужно создать на языке C# класс с тем же именем6, которое указано в файле разметки. Кроме того, в конструкторе нашего класса необходимо вызвать метод InitializeComponent7: using System; using System.Windows.Controls; using System.Windows; namespace EssentialWPF { public partial class MyWindow : Window { public MyWindow() { InitializeComponent(); } } } И чтобы завершить ассоциирование кода с разметкой, необходимо включить в проект файл на языке C#: <Project ...> ... <ItemGroup> <Compile Include=’mywindow.xaml.cs’ /> <Page Include=’mywindow.xaml’ /> <ApplicationDefinition Include=’app.xaml’ /> </ItemGroup> ... </Project> Поскольку ничего содержательного эта программа не делает, то запускать ее не интересно. Обычно связующим звеном между кодом и разметкой является об# работчик события. У элементов управления, как правило, есть одно или несколь# ко событий, которые могут быть обработаны в коде. Для задания обработчика нужно указать имя соответствующего метода в файле разметки: Принято называть файлы с кодом «<markupfile>.cs», так что файл с кодом, соответствующий файлу разметки mywindow.xml, будет называться mywindow.xml.cs. 7 Зачем нужен этот шаг, будет объяснено в главе 2. 6 46 Глава 1. Введение <Window x:Class=’EssentialWPF.MyWindow’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ Title=’Hello World’ > <WrapPanel> <Button Click=’HowdyClicked’>Howdy!</Button> <Button>A second button</Button> <TextBox>An editable text box</TextBox> <CheckBox>A check box </CheckBox> <Slider Width=’75’ Minimum=’0’ Maximum=’100’ Value=’50’ /> </WrapPanel> </Window> Теперь можно реализовать этот метод в файле с кодом: using System; using System.Windows.Controls; using System.Windows; namespace EssentialWPF { public partial class MyWindow : Window { public MyWindow() { InitializeComponent(); } void HowdyClicked(object sender, RoutedEventArgs e) { } } } Чтобы обратиться к элементу управления из файла с кодом, элементу необхо# димо присвоить имя: <Window x:Class=’EssentialWPF.MyWindow’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ Title=’Hello World’ > <WrapPanel> <Button Click=’HowdyClicked’>Howdy!</Button> <Button>A second button</Button> <TextBox x:Name=’_text1’>An editable text box</TextBox> <CheckBox>A check box </CheckBox> <Slider Width=’75’ Minimum=’0’ Maximum=’100’ Value=’50’ /> </WrapPanel> </Window> Теперь на это имя можно сослаться из кода: using System; Обзор WPF 47 using System.Windows.Controls; using System.Windows; namespace EssentialWPF { public partial class MyWindow : Window { public MyWindow() { InitializeComponent(); } void HowdyClicked(object sender, RoutedEventArgs e) { _text1.Text = ”Hello from C#”; } } } На рис. 1.16 показано, что произойдет, если запустить это приложение и щелк# нуть по кнопке Howdy! Помимо основных операций с элементами управления, менеджерами размеще# ния и событиями, в приложении, естественно, приходится работать с данными. Работа с данными Возможность работы с данными, включая привязку к ним, – неотъемлемая черта WPF. Даже если взглянуть на один из самых простых элементов управле# ния, мы увидим много разных способов привязки к данным: Button b = new Button(); b.Content = ”Hello World”; Здесь можно наблюдать по меньшей мере три вида привязки. Во#первых, сам спо# соб отображения кнопки определяется привязкой к данным. У каждого элемента уп# равления есть свойство Resources. Оно представляет собой словарь, в котором хранят# ся стили, шаблоны и другие данные. Элемент может привязываться к своим ресурсам. Во#вторых, содержимое, отображаемое на кнопке, – это объект типа System.Object. В большинстве элементов управления в WPF применяется так на# зываемая модель содержимого, которая позволяет задавать сложно структуриро# ванное содержимое элемента и отвечает за представление данных. Так, помимо строки на кнопке можно отображать почти любое содержимое. В#третьих, стандартная реализация отображения кнопки и модели содержи# мого позволяет использовать привязку к данным для связывания свойств эле# мента с его отображением. Рис. 1.16. Щелчок по кнопке вызывает изменения в другом элементе 48 Глава 1. Введение Чтобы ближе познакомиться с работой механизма привязки к данным в WPF, рассмотрим два сценария. Во#первых, зададим цвет фона кнопки: <Button Background=’Red’ /> Если нужно, чтобы такой же фоновый цвет был у нескольких кнопок, то про# ще всего поместить определение цвета в общедоступное место и настроить кноп# ки так, чтобы они обращались к этому месту. Именно для этого и предназначено свойство Resources. Чтобы определить ресурс, мы объявляем объект в свойстве Resources элемен# та управления и присваиваем его атрибуту x:Key: <Window x:Class=’EssentialWPF.ResourceSample’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ Title=’Hello World’ > <Window.Resources> <SolidColorBrush x:Key=’bg’ Color=’Red’ /> </Window.Resources> <!— ... описание других аспектов окна ... —> </Window> На именованный ресурс можно сослаться с помощью одного из расширений разметки: DynamicResource или StaticResource (подробное рассмотрение отло# жим до главы 6). <Window x:Class=’EssentialWPF.ResourceSample’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ Title=’Hello World’ > <Window.Resources> <SolidColorBrush x:Key=’bg’ Color=’Red’ /> </Window.Resources> <WrapPanel> <Button Background=’{StaticResource bg}’ Click=’HowdyClicked’>Howdy!</Button> <Button Background=’{StaticResource bg}’>A second button</Button> <TextBox x:Name=’_text1’>An editable text box</TextBox> <CheckBox>A check box </CheckBox> <Slider Width=’75’ Minimum=’0’ Maximum=’100’ Value=’50’ /> </WrapPanel> </Window> Обзор WPF 49 Рис. 1.17. Привязка к ресурсу Запустив эту программу, мы увидим, что цвет обеих кнопок один и тот же (рис. 1.17). Привязка к ресурсу – это сравнительно простой вид привязки. Можно также связывать свойства одного объекта со свойствами другого (или с объектами дан# ных). Например, можно связать текст, отображаемый в элементе TextBox, с со# держимым элемента CheckBox: <Window x:Class=’EssentialWPF.ResourceSample’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ Title=’Hello World’ > <Window.Resources> <SolidColorBrush x:Key=’bg’ Color=’Red’ /> </Window.Resources> <WrapPanel> <Button Background=’{StaticResource bg}’ Click=’HowdyClicked’>Howdy!</Button> <Button Background=’{StaticResource bg}’>A second button</Button> <TextBox x:Name=’_text1’>An editable text box</TextBox> <CheckBox Content=’{Binding ElementName=_text1,Path=Text}’ /> <Slider Width=’75’ Minimum=’0’ Maximum=’100’ Value=’50’ /> </WrapPanel> </Window> Если запустить эту программу и начать вводить текст, то содержимое флажка будет синхронно обновляться (рис. 1.18). Глубокая интеграция элементов управления с данными – основа развитого механизма визуализации данных. Помимо традиционных элементов управления, WPF обеспечивает прозрачный доступ к документам, мультимедийному содер# жимому и графике. Глава 1. Введение 50 Рис. 1.18. Связывание двух элементов управления Развитая интеграция Система визуализации в WPF поддерживает двумерную векторную графику, растровые изображения, текст, анимацию, видео, аудио и трехмерную графику. Все это интегрировано в единый механизм композиции, построенный поверх DirectX, что, в свою очередь, позволяет задействовать аппаратные акселераторы, реализованные в современных видеокартах. Знакомство с интеграцией мы начнем с создания прямоугольника. Но вместо того чтобы закрашивать его сплошным цветом, применим градиентную заливку с переходом от красного через белый к синему: <Window ... > <Window.Resources> <SolidColorBrush x:Key=’bg’ Color=’Red’ /> </Window.Resources> <DockPanel> <WrapPanel DockPanel.Dock=’Top’> <Button Background=’{StaticResource bg}’ Click=’HowdyClicked’>Howdy!</Button> <Button Background=’{StaticResource bg}’>A second button</Button> <TextBox x:Name=’_text1’>An editable text box</TextBox> <CheckBox Content=’{Binding ElementName=_text1,Path=Text}’ /> <Slider Width=’75’ Minimum=’0’ Maximum=’100’ Value=’50’ /> </WrapPanel> <Rectangle Margin=’5’> <Rectangle.Fill> <LinearGradientBrush> <GradientStop Offset=’0’ Color=’Red’ /> <GradientStop Offset=’.5’ Color=’White’ /> <GradientStop Offset=’1’ Color=’Blue’ /> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> </DockPanel> </Window> Обзор WPF 51 Рис. 1.19. Прямоугольник с градиентной заливкой Результат показан на рис. 1.19. Если изменить размер окна, то размер прямо# угольника тоже изменится, а переход цвета всегда будет начинаться и заканчи# ваться в противоположных углах. Следовательно, двумерная графика интегриро# вана с системой размещения. Можно пойти дальше и взять в качестве кисти на# бор элементов управления. В следующем примере мы присвоим панели WrapPanel имя и для заливки прямоугольника применим кисть VisualBrush. Та# кой кисти передается некий элемент управления, и она повторяет его изображе# ние, заполняя прямоугольник. С помощью свойств Viewport и TileMode мы мо# жем задать режим многократного повторения. 52 Глава 1. Введение Рис. 1.20. Применение кисти VisualBrush для заливки прямоугольника <Window ... > <Window.Resources> <SolidColorBrush x:Key=’bg’ Color=’Red’ /> </Window.Resources> <DockPanel> <WrapPanel x:Name=’panel’ DockPanel.Dock=’Top’> <Button Background=’{StaticResource bg}’ Click=’HowdyClicked’>Howdy!</Button> <Button Background=’{StaticResource bg}’>A second button</Button> <TextBox x:Name=’_text1’>An editable text box</TextBox> <CheckBox Content=’{Binding ElementName=_text1,Path=Text}’ /> <Slider Width=’75’ Minimum=’0’ Maximum=’100’ Value=’50’ /> </WrapPanel> Обзор WPF 53 <Rectangle Margin=’5’> <Rectangle.Fill> <VisualBrush Visual=’{Binding ElementName=panel}’ Viewport=’0,0,.5,.2’ TileMode=’Tile’ /> </Rectangle.Fill> </Rectangle> </DockPanel> </Window> Запустив эту программу и начав редактировать элементы, расположенные вдоль верхнего края, мы увидим, что прямоугольник изменяется (рис. 1.20). Та# ким образом, можно не только пользоваться двумерными изображениями для рисования элементов управления, но и сами элементы могут быть частями дву# мерных изображений. На самом деле, все элементы реализованы как набор дву# мерных изображений. Можно пойти еще дальше. WPF предоставляет также базовую поддержку трехмерной графики. Можно взять ту же самую визуальную кисть и воспользо# ваться ей как текстурой трехмерного изображения. Для создания трехмерной сцены необходимо пять компонентов: модель (форма), материал (чем покрыть форму), камера (откуда смотреть), свет (чтобы видеть) и окно проекции (внутри которого отображается сцена). В главе 5 мы будем рассматривать трехмерные сцены подробно, а пока отметим лишь, что в качестве материала модели можно пользоваться визуальной кистью: <Window ... > <Window.Resources> <SolidColorBrush x:Key=’bg’ Color=’Red’ /> </Window.Resources> <DockPanel> <WrapPanel x:Name=’panel’ DockPanel.Dock=’Top’> <Button Background=’{StaticResource bg}’ Click=’HowdyClicked’>Howdy!</Button> <Button Background=’{StaticResource bg}’>A second button</Button> <TextBox x:Name=’_text1’>An editable text box</TextBox> <CheckBox Content=’{Binding ElementName=_text1,Path=Text}’ /> <Slider Width=’75’ Minimum=’0’ Maximum=’100’ Value=’50’ /> </WrapPanel> <Viewport3D> <Viewport3D.Camera> <PerspectiveCamera LookDirection=’ .7, .8, 1’ Position=’3.8,4,4’ FieldOfView=’17’ UpDirection=’0,1,0’ /> </Viewport3D.Camera> <ModelVisual3D> 54 Глава 1. Введение <ModelVisual3D.Content> <Model3DGroup> <PointLight Position=’3.8,4,4’ Color=’White’ Range=’7’ ConstantAttenuation=’1.0’ /> <GeometryModel3D> <GeometryModel3D.Geometry> <MeshGeometry3D TextureCoordinates= ‘0,0 1,0 0, 1 1, 1 0,0 1,0 0, 1 0,0’ Positions= ‘0,0,0 1,0,0 0,1,0 1,1,0 0,1, 1 1,1, 1 1,1, 1 1,0, 1’ TriangleIndices=’0,1,2 3,2,1 4,2,3 5,4,3 6,3,1 7,6,1’ /> </GeometryModel3D.Geometry> <GeometryModel3D.Material> <DiffuseMaterial> <DiffuseMaterial.Brush> <VisualBrush Viewport=’0,0,.5,.25’ TileMode=’Tile’ Visual=’{Binding ElementName=panel}’ /> </DiffuseMaterial.Brush> </DiffuseMaterial> </GeometryModel3D.Material> </GeometryModel3D> </Model3DGroup> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> </DockPanel> </Window> На рис. 1.21 показано, что получается в результате. Как и при заливке двумер# ного прямоугольника, изменение элементов управления отражается на внешнем виде трехмерного объекта. Конечно, для описания трехмерной сцены требуется большой объем разметки. Поэтому, если вы собираетесь экспериментировать с трехмерной графикой, я настоятельно рекомендую воспользоваться каким#ни# будь специализированным редактором. И последнее, что хотелось бы упомянуть в связи с интеграцией. Все, что мы видели до сих пор, было более#менее статичным. Но в WPF все компоненты из# начально поддерживают анимацию. Анимация позволяет периодически изме# нять значение некоторого свойства. Чтобы анимировать нашу трехмерную сцену, добавим для начала преобразование поворота. Это преобразование позволяет по# вернуть трехмерную модель, изменив угол обзора. Затем мы анимируем изобра# жение, указав, что угол нужно изменять периодически: Обзор WPF Рис. 1.21. Элементы управления в качестве материала трехмерной формы <!— ...прочие аспекты сцены... —> <GeometryModel3D> <GeometryModel3D.Transform> <RotateTransform3D CenterX=’.5’ CenterY=’.5’ CenterZ=’ .5’> <RotateTransform3D.Rotation> <AxisAngleRotation3D x:Name=’rotation’ Axis=’0,1,0’ 55 56 Глава 1. Введение Angle=’0’ /> </RotateTransform3D.Rotation> </RotateTransform3D> </GeometryModel3D.Transform> <!— ...прочие аспекты сцены... —> Теперь можно приступать к определению анимации. Тут много разных дета# лей, но самым важным видом анимации является DoubleAnimation, когда изме# няется некое значение типа double. (Анимация вида ColorAnimation позволяет изменять цвет.) Мы анимируем угол поворота от #25 до 25. Смена направления вращения будет производиться автоматически, а на каждый поворот отводится 2.5 секунды. <Window ...> <!— ...прочие аспекты сцены... —> <Window.Triggers> <EventTrigger RoutedEvent=’FrameworkElement.Loaded’> <EventTrigger.Actions> <BeginStoryboard> <BeginStoryboard.Storyboard> <Storyboard> <DoubleAnimation From=’ 25’ To=’25’ Storyboard.TargetName=’rotation’ Storyboard.TargetProperty=’Angle’ AutoReverse=’True’ Duration=’0:0:2.5’ RepeatBehavior=’Forever’ /> </Storyboard> </BeginStoryboard.Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </Window.Triggers> <!— ...прочие аспекты сцены... —> При запуске этой программы вы увидите картину, изображенную на рис. 1.22, только анимированную. (Я пытался убедить издателей продавать вместе с каж# дым экземпляром книги ноутбук, чтобы вы могли воочию наблюдать анимацию, но они решили, что это слишком расточительно.) Интеграция пользовательского интерфейса, документов и мультимедийного содержимого встроены в WPF очень глубоко. Можно наделять кнопки трехмерными текстурами, а в качестве фона текстового поля использовать видео – возможно почти все. Такая гибкость облает большой мощностью, но может и вызывать сложности при неразумном использовании. Один из способов добиться развитого и в то же время последова# тельного изображения – система стилизации WPF. Обзор WPF 57 Рис. 1.22. Анимирование трехмерной сцены Будем стильными Стили – это механизм применения некоторого набора свойств к одному или нескольким элементам управления. Поскольку свойства – основа почти всех настроек в WPF, то таким образом можно описать практически все аспекты при# ложения. С помощью стилей можно создавать однородные темы и применять их к разным приложениям. Чтобы понять, как работают стили, модифицируем две красных кнопки. Во# первых, мы больше не будем ссылаться из каждой кнопки на ресурс, а перенесем задание цвета фона в определение стиля. Задав в качестве значения атрибута Key тип Button, мы говорим, что этот тип должен автоматически применяться ко всем кнопкам в окне. Глава 1. Введение 58 <Window ... > <Window.Resources> <SolidColorBrush x:Key=’bg’ Color=’Red’ /> <Style x:Key=’{x:Type Button}’ TargetType=’{x:Type Button}’> <Setter Property=’Background’ Value=’{StaticResource bg}’ /> </Style> </Window.Resources> <!— ... прочие аспекты окна ... —> <WrapPanel x:Name=’panel’ DockPanel.Dock=’Top’> <Button Click=’HowdyClicked’>Howdy!</Button> <Button>A second button</Button> <TextBox x:Name=’_text1’>An editable text box</TextBox> <CheckBox Content=’{Binding ElementName=_text1,Path=Text}’ /> <Slider Width=’75’ Minimum=’0’ Maximum=’100’ Value=’50’ /> </WrapPanel> <!— ... прочие аспекты окна ... —> </Window> Результат работы этой программы точно такой же, как показано на рис. 1.22. Интереса ради попробуем изменить свойство Template кнопки. Большинство элементов управления в WPF поддерживают шаблоны, позволяющие деклара# тивно изменять способ рисования элемента. В данном случае мы заменим станда# ртный вид кнопки эллипсом. Класс ContentPresenter сообщает шаблону, где размещать содержимое кноп# ки. Здесь для отображения одной#единственной кнопки мы применяем менеджер размещения, разные элементы управления и двумерную графику: <Style x:Key=’{x:Type Button}’ TargetType=’{x:Type Button}’> <Setter Property=’Background’ Value=’{StaticResource bg}’ /> <Setter Property=’Template’> <Setter.Value> <ControlTemplate TargetType=’{x:Type Button}’> <Grid> <Ellipse StrokeThickness=’4’> <Ellipse.Stroke> <LinearGradientBrush> <GradientStop Offset=’0’ Color=’White’ /> <GradientStop Offset=’1’ Color=’Black’ /> </LinearGradientBrush> </Ellipse.Stroke> <Ellipse.Fill> <LinearGradientBrush> <GradientStop Offset=’0’ Color=’Silver’ /> <GradientStop Offset=’1’ Color=’White’ /> </LinearGradientBrush> Обзор WPF 59 </Ellipse.Fill> </Ellipse> <ContentPresenter Margin=’10’ HorizontalAlignment=’Center’ VerticalAlignment=’Center’ /> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> На рис. 1.23 показано, что получается при запуске этой программы. Кнопки ос# таются активными; при нажатии кнопки Howdy! по#прежнему обновляется текс# товое поле (напомним, что код, реализующий такое поведение, был написан выше). Мы упомянули большую часть возможностей WPF, но пока лишь очень пове# рхностно. Перед тем как завершить введение, поговорим о том, как сконфигури# ровать компьютер для сборки и запуска тех чудесных программ, которые мы со# бираемся написать. Инструменты для построения приложения Чтобы откомпилировать и запустить любую программу из этой книги, нужно иметь базовый комплект инструментов и понимать, как ими пользоваться. Для создания полной среды разработки не нужно практически ничего, кроме соедине# ния с Интернетом, поскольку новые продукты, входящие в состав Visual Studio Express распространяются бесплатно! .NET Framework 3.08; Windows Software Development Kit9; ваш любимый текстовый редактор (я использую Visual C# Express10). Дополнительно можно скачать .NET Framework 3.0 Extensions для Visual Studio (в настоящее время этот продукт имеет кодовое название Orcas), который пока распространяется в виде общедоступного технологического анонса (CTP) следующей версии Visual Studio. Со временем этот пакет будет заменен новой версией Visual Studio со встроенной поддержкой разработки в .NET Framework 3.0. В ходе краткого обзора WPF мы рассмотрели основы создания файла проек# та для компиляции WPF#приложений. Установив расширения для Visual Studio, вы сможете поручить все манипуляции с этим файлом самой Visual Studio. Аль# тернативно для сборки проектов можно воспользоваться программой Microsoft Expression Blend (кодовое название Sparkle). Два наиболее полезных источника информации по API – это документация, поставляемая в составе Windows SDK, и инспектор сборок Reflector11. NET Framework 3.0 без ограничений на свободное распространение можно найти по адресу http://msdn.microsoft.com/ windowsvista/downloads/products/default.aspx. 9 Windows SDK находится по адресу http://msdn.microsoft.com/ windowsvista. 10 Visual C# Express находится по адресу http://msdn.microsoft.com/vstudio/express/visualCsharp/default.aspx. 11 Reflector доступен по адресу http://www.aisto.com/roeder/dotnet. 8 Глава 1. Введение 60 Рис. 1.23. Кнопки со специальным шаблоном, определенным в стиле Чего мы достигли В этой главе мы обсудили, почему корпорация Microsoft решила разработать платформу WPF, и бегло рассмотрели основные аспекты этой платформы. Мы узнали, как пользоваться инструментами, необходимыми для построения WPF# приложений, и привели ссылки на некоторые ресурсы, откуда можно взять тре# буемое программное обеспечение. 61 Глава 2. Приложения У каждого из нас есть собственное определение того, что такое приложение; Мое любимое звучит так: «программа, основное назначение которой – взаимо# действие с человеком». Платформа Windows Presentation Foundation вся посвя# щена представлению информации в удобном для человека виде. Неудивительно поэтому, что я считаю правильным начать углубленное ее исследование именно с уровня приложений. В своей модели приложения WPF стремится пройти по узкой тропке, с одной стороны, предоставив набор гибких служб для построения приложений, а, с дру# гой, – не вводя излишне жестких правил, из#за которых нельзя будет создавать новые решения. Было решено предложить набор интегрированных служб, кото# рыми разработчики могли бы овладевать постепенно. Приложения для WPF сос# тоят из различных компонентов пользовательских интерфейсов, ресурсов, соеди# нений со службами и данными и конфигурационной информации. При рассмот# рении структуры приложения, полезно выделять высокоуровневые строитель# ные блоки пользовательского интерфейса (окна, страницы и элементы управле# ния) и службы прикладного уровня (навигацию, ресурсы, конфигурацию и раз# мещение). Все это мы рассмотрим в данной главе. Принципы организации приложения При разработке WPF мы стремились создать облегченную модель приложе# ния. Мы понимали, что базовая платформа должна быть максимально гибкой, од# нако хотели придерживаться некоторых основополагающих принципов. Было ясно, что надо построить такую систему, которая позволяла бы переходить от «тонких» Web#приложений к полномасштабным приложениям для персональ# ного компьютера. Мы ставили себе целью не только добиться такой масштабиру# емости, но и взять все лучшее из обоих миров и предоставить это в распоряжение разработчика программ как того, так и другого вида. Масштабируемые приложения Один из базовых принципов WPF – обеспечить масштабируемость, то есть возможность создавать широкий спектр приложений – от «тонких» клиентов, ра# ботающих в браузере, и небольших программ, легко развертываемых на персо# нальном компьютере, до полномасштабных приложений, требующих наличия специального установщика. Поэтому несмотря на то, что всегда создается объект Application и запускается его метод Run, простое WPF#приложение можно напи# сать на одном лишь языке разметки. Глава 2. Приложения 62 Создадим новый файл HelloWorld.xaml и поместим в него следующий текст: <Page xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ WindowTitle=’Hello World’> <TextBlock FontSize=’24’>Hello World</TextBlock> </Page> Для запуска этого приложения достаточно дважды щелкнуть мышью по XAML#файлу, и мы увидим окно, изображенное на рис. 2.1. Впрочем, это прос# тейшее приложение мало что умеет, кода#то в нем нет. Мы можем реализовать несложное интерактивное взаимодействие с помощью анимации и привязки к дан# ным, но, чтобы наделить приложение логикой, необходимо добавить код. Чтобы написать для страницы код, нужно присвоить этой странице имя в смыс# ле CLR, добавив атрибут x:Class. Мы можем также включить атрибут x:Name в тег TextBlock, чтобы в дальнейшем получить к нему доступ из кода. <Page x:Class=’EssentialWPF.HelloWorld’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ WindowTitle=’Hello World’> <TextBlock x:Name=’text1’ FontSize=’24’>Hello World</TextBlock> </Page> Рис. 2.1. Простейшее приложение, исполняемое в браузере Теперь можно написать для этой страницы код. Подойдет любой .NET#сов# местимый язык программирования; мы остановимся на C#. В качестве текста бу# дем показывать текущее время: using System; using System.Windows; using System.Windows.Controls; Принципы организации приложения 63 namespace EssentialWPF { public partial class HelloWorld : Page { public HelloWorld() { InitializeComponent(); text1.Text = «Now is: « + DateTime.Now.ToString(); } } } Раз имеется код, проект необходимо откомпилировать. В любой компилируе# мый проект, который порождает исполняемую программу (как, например, наше приложение), имеет смысл включить определение приложения. Для этого мы воспользуемся базовым объектом Application и зададим для него атрибут StartupUri в нашем XAML#файле: <Application xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ StartupUri=’helloworld.xaml’ /> Последний шаг преобразования приложения, состоящего только из разметки, в приложение, включающее код, – компиляция проекта. Для этого необходим файл проекта, содержащий инструкции программе MSBuild. По большей части содер# жимое файла проекта1 стандартно (и генерируется автоматически инструментами типа Visual Studio). Но наши три файла мы должны включить самостоятельно: <Project DefaultTargets=»Build» xmlns=»http://schemas.microsoft.com/developer/msbuild/2003»> <!— ...опущенные части файла проекта ... —> <PropertyGroup> <HostInBrowser>true</HostInBrowser> </PropertyGroup> <ItemGroup> <Page Include=»HelloWorld.xaml» /> <ApplicationDefinition Include=»App.xaml» /> <Compile Include=»HelloWorld.xaml.cs» /> </ItemGroup> <!— ...опущенные части файла проекта ... —> </Project> В ходе построения проекта будет создан файл HelloWorld.xbap (XBAP – XAML Browser Application – XAML#приложение для браузера). Если два раза щелкнуть по нему мышью, то появится окно, показанное на рис. 2.2. XBAP#файлы обладают куда более широкими возможностями, чем традици# онные приложения на базе HTML. Они позволяют обмениваться данными с Web#серверами, получать доступ к безопасному локальному хранилищу и обра# Манифест проекта должен быть подписан, поэтому необходимо включать в файл элементы SignManifests, ManifestKeyFile и ManifestCertificateThumbprint. 1 64 Глава 2. Приложения щаться к значительной части функциональности каркаса .NET Framework2. Если программе необходимо больше возможностей, чем предоставляется приложени# ям, исполняемым в браузере, то ее можно преобразовать в персональное прило# жение, изменив атрибут HostInBrowser в файле проекта: <Project DefaultTargets=»Build» xmlns=»http://schemas.microsoft.com/ developer/msbuild/2003»> <!— ...опущенные части файла проекта ... —> <PropertyGroup> <HostInBrowser>false</HostInBrowser> </PropertyGroup> <!— ...опущенные части файла проекта ... —> </Project> Рис. 2.2. XBAP)приложение, исполняемое в браузере Пересобрав проект и запустив программу HelloWorld.exe, мы увидим карти# ну, изображенную на рис. 2.3. У этого принципа проектирования масштабируемых приложений есть ряд ин# тересных аспектов. Во#первых, масштабируемым является механизм развертыва# ния. Приложения, состоящие из одной лишь разметки, развертываются с помощью простого HTTP#запроса. Приложения, работающие в браузере, временно развер# тываются3 с помощью одного из вариантов технологии ClickOnce, а для персональ# ных приложений можно применять ClickOnce или установку из MSI#пакета. 2 XBAP)файлы обычно исполняются с уровнем безопасности «Интернет», поэтому их функциональ) ность ограничена операциями, которые может выполнять частично доверенный код. 3 Я написал «временно развертываются», поскольку приложения, исполняемые в браузере, не уста) навливаются на локальную машину на постоянной основе. Хотя они и развертываются на клиенте, но помещаются в кэш, из которого в конечном итоге выталкиваются так же, как Web)страницы вы) талкиваются из кэша Internet Explorer. Принципы организации приложения 65 Рис. 2.3. Персональное приложение Помимо развертывания, масштабируется также способ размещения. Пользо# вательский интерфейс приложения может быть размещен в браузере или в окне персонального приложения. Внимательный взгляд на рис. 2.3 показывает, что да# же в последнем случае поддерживается навигация. Это подводит нас еще к одно# му фундаментальному принципу модели приложения: интеграции идей Web в персональные приложения. Стиль Web На модель приложения оказало большое влияние желание объединить луч# шее из практики программирования для Windows и Web. Мы уже видели приме# ры такой интеграции: размещение в браузере и развертывание в Web. Но WPF идет еще дальше, поддерживая единую концепцию навигации. Вне зависимости от того, работает ли приложение в браузере или в окне пер# сонального приложения, в него можно легко включить навигацию. Продолжая предыдущий пример, создадим вторую страницу second#page.xaml: <Page xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ WindowTitle=’Second Page’> <TextBlock FontSize=’24’>Welcome to page 2</TextBlock> </Page> С помощью элемента управления Hyperlink мы можем добавить ссылку на первую страницу: <Page ... WindowTitle=’Hello World’> <StackPanel> <TextBlock x:Name=’text1’ FontSize=’24’>Hello World</TextBlock> <TextBlock FontSize=’24’> Глава 2. Приложения 66 <Hyperlink NavigateUri=’secondpage.xaml’> Goto Page 2 </Hyperlink> </TextBlock> </StackPanel> </Page> Рис. 2.4. Персональное приложение, включающее навигацию Запустив эту программу (рис. 2.4), мы увидим, что даже в окне персонально# го приложения доступны все возможности навигационной системы. Обратите внимание, что URI (универсальный идентификатор ресурса) второй страницы равен second#page.xaml. Как же был определен этот URI? В Web#прило# жениях URI применяются для ссылки на произвольные ресурсы. Кроме того, в них в качестве базового используется URI корневой страницы приложения; ина# че говоря, относительные URI берут начало именно от этого корня. Принятая в WPF модель ссылок на ресурсы очень много позаимствовала от Web, но сделала еще один шаг: интегрировала компилируемые типы (например, откомпилирован# ные версии XAML#файлов), свободные ресурсы (изображения на диске, страницы в Интернете и т.д.) и пакетные ресурсы (например, встроенные в приложение). Так, мы можем добавить рисунок на нашу вторую страницу, сославшись на не# который URI в Интернете: <Page xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ WindowTitle=’Second Page’> <TextBlock FontSize=’24’>Welcome to page 2 <Image Source=’http://blog.simplegeek.com/images/img_1476small.png’ /> </TextBlock> </Page> Эта программа выведет окно, показанное на рис. 2.5, предварительно загрузив рисунок. Приложения в стиле Web распространяются все шире и шире. WPF учла эту тенденцию и предлагает многочисленные достоинства Web#модели для создания Принципы организации приложения 67 развитых персональных приложений. Однако одним из основополагающих принципов WPF является также и включение лучших черт программирования для Windows. Рис. 2.5. Ссылка на ресурс в Интернете Стиль персональных приложений Многие идеи программирования персональных приложений – многоокон# ность, всплывающие подсказки, интеграция с меню Пуск, управление буфером обмена, поддержка работы в автономном режиме, интеграция с файловой систе# мой и т.д. – настолько укоренились в сознании разработчиков, что мы даже не ду# маем о них. Один из наиболее наглядных стилистических признаков персонального при# ложения – оконная модель. В приложении на базе User32 легко реализуются од# нодокументный (SDI) или многодокументный (MDI) интерфейс, диалоговые ок# на и мастера (таблица 2.1). Microsoft Office Word 2003 построен в соответствии с однодокументной моделью: каждый документ открывается в отдельном окне верхнего уровня, а вместе с закрытием последнего документа завершается и само приложение. В Adobe Photoshop CS используется многодокументная модель: су# ществует одно окно верхнего уровня, у которого может быть много дочерних окон, в каждом из которых отображаются различные виды открытого документа. Photoshop завершается, когда закрывается окно верхнего уровня. Глава 2. Приложения 68 Модель Определение Типичное применение Однодокументный Каждый документ отображается в интерфейс (SDI) отдельном окне верхнего уровня. Для переключения между документами используется системная панель задач. Приложение завершается вместе с закрытием последнего документа. Редакторы общего назначения, предназначенные для работы с документами, электронными таблицами и электронной почтой (например, Microsoft Office Word 2003). Диалоговое окно Приложение исполняется как обособленное логическое действие. Создается одно окно верхнего уровня, и приложение завершается, когда подходит к концу логическое действие. Редко используемые инструменты настройки (например, для изменения параметров дисплея, добавления новых пользователей, установки обновлений). SDI#навигация Каждый экземпляр окна навигации соответствует логическому сеансу браузера. Нечасто выполняемые задачи (например, заказ билетов, покупки в Интернет#магазине). Навигация в духе браузера Приложение запускается путем щелчка по ссылке. Различные его части активируются щелчками по ссылкам внутри единственного экземпляра браузера. Приложение никогда не завершается, но пользователь может уйти в другое место. Навигация по информационному пространству, скажем, по энциклопедии или хранилищу документов (например, www.msn.com). Таблица 2.1. Оконные модели По мере развития Интернета появилась новая платформа для создания приложе# ний: браузер. В такого рода приложениях используется лишь одна оконная модель – навигационная. Есть примеры Web#приложений, в которых реализована модель ди# алога, однодокументный и даже многодокументный интерфейс, но для этого приш# лось заново реализовать многие механизмы, характерные для таких моделей. WPF стремится унифицировать все оконные модели и дать разработчикам возможность смешивать в одном приложении разные модели, а также создавать свои собственные. Но при всей гибкости WPF нужно ясно осознавать, какая мо# дель для каких задач наиболее приспособлена. WPF сознательно не поддерживает оконную модель MDI. Когда начиналась ра# бота над WPF (в конце 2000 года), наметился отход от MDI#приложений. Реализа# ция поддержки MDI требует много работы, и разработчики пришли к выводу, что игра не стоит свеч, особенно если учесть стремление поддержать новые стили при# ложений (многооконный интерфейс с вкладками в духе Visual Studio 2005, навига# цию a la Internet Explorer и т.д.). Когда в 2003 году вышел первый анонс WPF, пос# тупило много просьб включить поддержку MDI, но к тому моменту интегрировать Объект Application 69 ее в продукт было уже поздно. Есть два обходных пути решения этой проблемы: воспользоваться реализацией MDI, имеющейся в Windows Forms, или написать собственный менеджер дочерних окон. Для этого нужно не так уж много кода, но получившийся менеджер вряд ли будет полнофункциональным (легко написать 80% оконного менеджера и очень трудно оставшиеся 20%). Если ваше приложение не может обойтись без MDI, я бы рекомендовал обратиться к Windows Forms. Авторы приложений не обязаны ограничиваться единственной моделью во всем приложении. Например, почтовая программа могла бы использовать нави# гацию в стиле браузера или SDI для просмотра сообщений, а для составления но# вого сообщения переключаться на модель SDI. Программа для покупок в Интер# нете может работать как приложение в SDI#навигацией, пока пользователь изу# чает витрину магазина, а в момент оформления заказа, когда запрашиваются де# тали метода платежа и доставки, переключаться в режим диалога. Три названных принципа – масштабируемость, стиль Web и стиль персональ# ных приложений – пронизывают почти все аспекты модели приложения. Рас# смотрев суть этих принципов, мы можем перейти к деталям, чтобы на деле уви# деть, как все это работает. Объект Application Объект Application отвечает за управление временем жизни приложения, отс# леживает видимые окна, освобождает ресурсы и контролирует глобальное состо# яние приложения. Логически WPF#приложение начинается с вызова метода Run объекта Application. using System; using System.Windows; namespace EssentialWPF { static class Program { [STAThread] static void Main() { Application app = new Application(); Window w = new Window(); w.Title = «Hello World»; w.Show(); app.Run(); } } } На этом простом примере видно, как создается и начинает исполняться объект приложения. Вызов метода Run обычно находится в последней строке функции, являющейся точкой входа4. Метод Run начинает посылать события и сообщения 4 Маркер [STAThread] перед точкой входа обязателен. Как и в Windows Forms, а еще раньше в User32, WPF требует, чтобы поток пользовательского интерфейса находился в однопоточном апартаменте (STA), поскольку многие компоненты, которыми пользуется WPF (например, Clipboard) не могут ра) ботать иначе. Единообразия ради команда WPF решила сделать требование к STA непререкаемым. Глава 2. Приложения 70 компонентам WPF#приложения5. Он будет работать до тех пор, пока приложение не завершится. В каждый момент времени может быть активен только один объект Application (на самом деле, после начала работы приложения второй такой объект даже создать не получится). С помощью статического свойства Application.Current можно в любом месте программы получить доступ к текущему объекту Application. Определение Для инкапсуляции логики запуска WPF#приложения обычно создают подк# ласс класса Application: // program.cs using System; using System.Windows; namespace EssentialWPF { static class Program { [STAThread] static void Main() { MyApp app = new MyApp(); app.Run(); } } class MyApp : Application { public MyApp() { Window w = new Window(); w.Title = ”Hello World”; w.Show(); } } } Одна из целей WPF состоит в том, чтобы программист писал как можно мень# ше стандартного кода. Другая цель – всюду, где только можно, разрешить декла# ративное программирование. В любом WPF#приложении приходится писать ка# кой#то код в функции Main: задавать атрибут STAThread, создавать объект Application и вызывать метод Run. Но WPF не требует, чтобы мы писали его сно# ва и снова в каждой программе, а позволяет определить приложение в виде фай# ла разметки. Напоследок добавим два новых файла в файл проекта: <!— myapp.xaml —> <Application x:Class=’EssentialWPF.MyApp’ ... /> // myapp.xaml.cs using System; using System.Windows; 5 Точнее говоря, Run запускает диспетчер, который отвечает за отправку событий и сообщений. Подробнее диспетчер рассматривается в приложении. Объект Application 71 namespace EssentialWPF { partial class MyApp : Application { public MyApp() { Window w = new Window(); w.Title = ”Hello World”; w.Show(); } } } <!— sample.csproj —> <Project DefaultTargets=’Build’ xmlns=’http://schemas.microsoft.com/developer/msbuild/2003’> ... <ItemGroup> <ApplicationDefinition Include=’myapp.xaml’ /> <Compile Include=’myapp.xaml.cs’ /> </ItemGroup> ... </Project> Система построения получает на входе этот файл и генерирует стандартный код, который мы ранее написали вручную. Ниже приведен фрагмент сгенериро# ванного кода: namespace EssentialWPF { /// <summary> /// MyApp /// </summary> public partial class MyApp : System.Windows.Application { /// <summary> /// Application entry point /// </summary> [System.STAThreadAttribute()] [System.Diagnostics.DebuggerNonUserCodeAttribute()] public static void Main() { EssentialWPF.MyApp app = new EssentialWPF.MyApp(); app.Run(); } } } Обратите внимание, что в сгенерированном коде написанная вручную функ# ция Main подменена6. 6 Как мы увидим ниже, Application — единственный элемент разметки, для которого не требуется вы) зывать метод InitializeComponent. 72 Глава 2. Приложения Время жизни В последней строке функции Main производится обращение к методу Application.Run. Рано или поздно этот метод должен вернуть управление, иначе приложение будет работать вечно. Одна из основных задач объекта Application зак# лючается в том, чтобы контролировать время жизни процесса. Конструирование этого объекта знаменует начало работы приложения, а возврат из метода Run – его завершение. Между этими двумя моментами протекает жизнь приложения. Все WPF#приложения устроены одинаково: Конструируется объект Application. Вызывается его метод Run. Возбуждается событие Application.Startup. Пользовательский код конструирует один или несколько объектов Window. Вызывается метод Application.Shutdown. Вызывается метод Application.Exit. Метод Run возвращает управление. Инициализировать приложение можно двумя способами: (1) в конструкторе объ# екта Application или (2) в обработчике события Startup. Последнее предпочтительнее, так как в этот момент внутренняя инициализация объекта Application уже завершена (скажем, внутри конструктора еще не установлено свойство Application.Current): <!— myapp.xaml —> <Application x:Class=’EssentialWPF.MyApp’ ... Startup=’MyApp_Startup’ /> // myapp.xaml.cs using System; using System.Windows; namespace EssentialWPF { partial class MyApp : Application { public MyApp() { } void MyApp_Startup(object sender, StartupEventArgs e) { Window w = new Window(); w.Title = ”Hello World”; w.Show(); } } } В какой#то момент метод Run должен вернуть управление, а для этого необхо# димо вызвать метод Application.Shutdown. Однако можно организовать програм# му так, чтобы метод Shutdown вызывался автоматически, и скоро – когда мы бу# дем говорить об объекте Window – я покажу, как это сделать. Возможно, вы об# Объект Application 73 ратили внимание, что в описании жизненного цикла приложения кое#что пропу# щено. Как говорится, «день у каждого бывает, когда дождь не затихает»*. Как пра# вило, разработчик рано или поздно сталкивается с проблемой – то ли потому что в программе есть ошибка, то ли из#за непредвиденных действий пользователя или (не дай бог!) в результате ошибки в самой платформе. Поэтому всегда нуж# но включать код для обработки возможных ошибок. Обработка ошибок Обработка ошибок и исключений в приложении – тема достаточно обширная для целой книги. Программист должен решить, что делать в случае возникнове# ния ошибки, и понимать философию, которой мы руководствовались при проек# тировании этого аспекта WPF. Были приложены все усилия к тому, чтобы вос# станавливать нормальную работу после возможно большего числа типов исклю# чений. Тут, правда, есть опасность порочного круга, поскольку, говоря о возмож# ности восстановления, мы тем самым ограничиваем типы исключений, против которых вооружены! Проблема в том, что существуют такие исключения, после которых восстановление невозможно. Самые очевидные примеры: StackOverflow Exception, OutOfMemoryException и ThreadAbortException. Поток аварийно завершается лишь в том случае, когда приложение вызывает ме# тод Thread.Abort. Если этого не делать, то исключение ThreadAbortException никогда и не возникнет. Переполнение стека чаще всего свидетельствует о бесконечной рекур# сии, хорошего в этом мало. Самым интересным из трех, наверное, является исключе# ние OutOfMemoryException. Когда системе не хватает памяти – по#настоящему не хватает, – может оказаться, что CLR не в состоянии выделить больше ни единого бай# та7. В таком случае невозможна своевременная компиляция, нельзя выполнить ни упаковку переменных, ни получить память для новых объектов. Если не считать этих трех особых случаев, то WPF всегда возвращается в непротиворечивое состояние пос# ле возникновения исключения. Это означает, что разработчик может выполнять собственную логику восстановления, предполагая, что сама платформа не пострадала. Не так уж трудно написать код обработки ошибок при вызове метода. Для это# го применяется конструкция try/catch/finally. Проблема в том, что делать, когда никто не обрабатывает исключение, – либо потому что оно возникло асинхронно, либо потому что осталось не перехваченным. В этом случае WPF по умолчанию аварийно завершает приложение. * Цитата из стихотворения Г. Лонгфелло «The rainy day» (перевод Ч. Самуил) (Прим. перев.) 7 В CLR есть механизм — область ограниченного выполнения (CER), который гарантирует возмож) ность выполнения даже при отсутствии памяти. Код, исполняемый внутри CER, не должен выделять память, упаковывать значения и вызывать методы, не являющиеся частью CER. Писать такой код очень трудно, поэтому он обычно встречается лишь на самых нижних уровнях платформы (менее сотни методов во всем каркасе .NET пользуются CER). Вместо этого WPF задействует механизм вентилей памяти (memory gate), который приводит к возбуждению восстановимого исключения еще до того, как ресурсы будут полностью исчерпаны. Это прогностический механизм (производит) ся оценка объема доступных ресурсов, и отказ случается еще до катастрофической нехватки памя) ти), поэтому гарантировать успех во всех случаях он не может. Задача вентилей памяти — умень) шить вероятность возникновения ситуации, когда памяти больше нет вообще. Хотя мы сделали все возможное, чтобы избежать невосстановимых исключений, особого интереса они не представляют. Коль скоро восстановление все равно невозможно, процесс просто завершается. Глава 2. Приложения 74 Объект Application предоставляет событие DispatcherUnhandledException8, которое соответствует именно таким обстоятельствам. Подписавшись на это со# бытие, приложение может реализовать любую политику обработки исключений, достигших уровня самого приложения: <!— MyApp.xaml —> <Application ... DispatcherUnhandledException=’Failure’ /> // MyApp.xaml.cs void Failure(object sender, DispatcherUnhandledExceptionEventArgs e) { // прикладная логика } Диспетчер – это часть системы, отвечающая за отправку событий и сообще# ний различным компонентам. Рассматриваемое событие посылается, когда дис# петчер видит необработанное приложением исключение. В аргументе типа DispatcherUnhandledExceptionEventArgs передается самое возникшее исключе# ние, а также флаг Handled, которому можно присвоить значение true и тем самым показать, что исключение следует игнорировать, а приложение должно продол# жать работу: public class DispatcherUnhandledExceptionEventArgs : DispatcherEventArgs { public Exception Exception { get; } public bool Handled { get; set; } } Мы могли бы реализовать тактику, состоящую в том, что любая ошибка игно# рируется, но протоколируется, а пользователю предлагается отправить отчет об ошибке системному администратору: void Failure(object sender, DispatcherUnhandledExceptionEventArgs e) { using (StreamWriter errorLog = new StreamWriter(«c:\\error.log», true)) { errorLog.WriteLine(«Error @ « + DateTime.Now.ToString(«R»)); errorLog.WriteLine(e.Exception.ToString()); } e.Handled = true; MessageBox.Show(«Произолша ошибка, отправьте системному « + «администратору содержимое файла ‘c:\\error.log’»); «contents of the file ‘c:\\error.log’»); } 8 Здесь слово dispatcher относится к высокоуровневому объекту обработки сообщений, который и делает всю работу внутри метода Application.Run. Объект Application 75 К тому моменту, когда исключение преобразуется в событие Dispatcher UnhandledException, у автора приложения остается не так уж много вариантов действий; о контексте, в котором произошла ошибка, известно очень мало (только сам объект исключения), и повторить операцию уже невозможно (если только внутреннее состояние не говорит о том, что было предпринято ранее). По сути де# ла, приложение может как#то сообщить об исключении, проигнорировать его, ава# рийно завершиться или выполнить ту или иную комбинацию вышеописанного. Иногда при разработке приложения об обработке ошибок забывают, хотя это очень важный аспект. О нем надо помнить и планировать адекватные действия на всех этапах жизненного цикла приложения. Управление состоянием Состояние приложения, как правило, сосредоточено в компонентах пользова# тельского интерфейса верхнего уровня. Например, бывает нужно поддерживать спи# сок открытых документов или следить за текущим состоянием сетевого соединения. Хранить состояние удобно в объекте Application, поскольку в этом случае оно дос# тупно глобально (с помощью статического свойства Application.Current) на протяже# нии всего времени работы приложения. Простейший способ сохранить состояние приложения – воспользоваться свойством Properties объекта Application. Оно имеет тип System.Collections.IDictionary, следовательно, в нем можно хранить любой объ# ект, сопоставив ему в качестве ключа любой другой объект, обычно строку. Продолжая приведенный выше пример обработки ошибок, мы можем кэши# ровать последнюю возникшую в приложении ошибку: // myapp.xaml.cs public partial class MyApp : Application { .. void Failure(object sender, DispatcherUnhandledExceptionEventArgs e) { using (StreamWriter errorLog = new StreamWriter(«c:\\error.log», true)) { errorLog.WriteLine(«Error @ « + DateTime.Now.ToString(«R»)); errorLog.WriteLine(e.Exception.ToString()); } e.Handled = true; this.Properties[«LastError»] = e.Exception; } .. } Поскольку Properties – просто словарь, в котором и ключ, и значение имеют тип object, то приходится постоянно выполнять приведение типов. Вместо этого можно определить свойство или поле в самом объекте Application: // myapp.xaml.cs public partial class MyApp : Application { private Exception _lastError; 76 Глава 2. Приложения public Exception LastError { get { return _lastError; } set { _lastError = value; } } ... void Failure(object sender, DispatcherUnhandledExceptionEventArgs e) { using (StreamWriter errorLog = new StreamWriter(«c:\\error.log», true)) { errorLog.WriteLine(«Error @ « + DateTime.Now.ToString(«R»)); errorLog.WriteLine(e.Exception.ToString()); } e.Handled = true; this.LastError = e.Exception; } ... } По мере расширения состояния приложения начинают возникать общие воп# росы. Следует ли сохранять состояние между запусками приложения? Связано ли состояние с конкретным пользователем? Задается ли состояние только на эта# пе компиляции (например, изображение)? Свойства объекта Application не дают ответов на эти вопросы; для многих видов состояния приходится обращаться к ресурсам и службам конфигурирования. Ресурсы и конфигурирование Есть много способов классифицировать состояние приложения, но я предпо# читаю рассматривать только три основных категории: конфигурация, содержи# мое и документ. В .NET поддерживаются все три модели. Конфигурация Состояние, сводящееся к конфигурации, – это набор параметров, ассоциирован# ный с конкретным пользователем или компьютером. Обычно его может изменять пользователь или администратор во время выполнения или на этапе развертыва# ния. Средства конфигурирования в .NET эволюционировали со времен версии .NET 1.0, и сейчас мы познакомимся с тем, как включить их в WPF#приложение. Для управления сохраняемыми параметрами приложения прекрасно подой# дет API, находящийся в пространстве имен System.Configuration. Углубленное изучение системы конфигурирования не входит в задачу этой книги, но пони# мать, как эта система интегрируется с объектом Application, необходимо. Прежде всего, необходимо определить объектную модель параметров. Это можно сделать либо с помощью конструктора параметров в Visual Studio 2005, либо написав соответствующий класс на любом .NET#совместимом языке. Мы определим объектную модель на C#, поставив себе целью запомнить, сколько раз запускалась программа: Ресурсы и конфигурирование 77 public class AppSettings { int _runCount; public int RunCount { get { return _runCount; } set { _runCount = value; } } } Здесь создано свойство для хранения информации о состоянии. Чтобы сде# лать его частью системы конфигурирования, нужно произвести наш класс от класса SettingsBase (или ApplicationSettingsBase) и предоставить некоторые ме# таданные, описывающие свойство. Вместо того чтобы хранить значение в локаль# ном поле, необходимо воспользоваться хранилищем, встроенным в тип SettingsBase, чтобы система могла отслеживать изменения значения: public class AppSettings : ApplicationSettingsBase { public AppSettings() : base() { } [UserScopedSetting] [DefaultSettingValue(«0»)] public int RunCount { get { return (int)this[«RunCount»]; } set { this[«RunCount»] = value; } } } Параметры могут относиться к одному пользователю или ко всем пользовате# лям (параметры уровня приложения). В данном случае мы решили, что у каждо# го пользователя будет свое значение параметра, поэтому применили к свойству атрибут UserScopedSetting. После определения объектной модели осталось сделать еще две вещи: настро# ить привязки к конфигурационному файлу и дать к ним доступ через объект Application. Привязки сообщают системе конфигурирования о том, как отобра# зить конфигурационный файл на только что определенную объектную модель. Чтобы задать привязки, нужно зарегистрировать класс AppSettings в системе конфигурирования, добавив секцию в файл app.config. Этот файл в .NET являет# ся стандартным местом для хранения всей конфигурационной информации при# ложения. Чтобы включить файл app.config в наш проект, мы должны добавить его как элемент None (согласен, это не очень логично): <ItemGroup> <None Include=’app.config’ /> </ItemGroup> На этапе компиляции система построения автоматически преобразует файл app.config в файл <OurProgramName>.exe.config. После компиляции конфигура# ционный файл будет выглядеть примерно так: Глава 2. Приложения 78 <?xml version=’1.0’ encoding=’utf 8’ ?> <configuration> <configSections> <sectionGroup name=’userSettings’ type=’System.Configuration.UserSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089’> <section name=’EssentialWPF.AppSettings’ type=’System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089’ allowExeDefinition=’MachineToLocalUser’ requirePermission=’false’ /> </sectionGroup> ... </configSections> ... </configuration> В этот момент мы можем откомпилировать приложение и воспользоваться новым объектом параметров. Для этого нужно просто создать этот объект. А что# бы дать к нему доступ из объекта Application, мы создадим открытое свойство эк# земпляра: public partial class MyApp : Application { AppSettings _settings = new AppSettings(); public AppSettings Settings { get { return _settings; } } public MyApp() { this.Exit += MyApp_Exit; this.Startup += AppStartup; } void MyApp_Exit(object sender, ExitEventArgs e) { Settings.Save(); } void AppStartup(object sender, StartupEventArgs args) { Window w = new Window(); w.Title = «You ran this « + (Settings.RunCount++) + « times»; w.Show(); } } Ресурсы и конфигурирование 79 Если несколько раз запустить это приложение, то мы увидим окно, изобра# женное на рис. 2.6. Если давать доступ к параметрам непосредственно из объекта Application, то любой объект в WPF#приложении сможет к ним обратиться. Вы# зов метода Settings.Save из события Exit сохраняет параметры при выходе из приложения. Вызывать Settings.Save можно в любой момент; Например, прог# рамма Microsoft OneNote сохраняет параметры во время работы, чтобы гаранти# ровать целостность состояния даже в случае аварийного завершения. Рис. 2.6. Запуск приложения с конфигурационными параметрами Состояние, связанное с содержимым Состояние, связанное с содержимым, обычно называют ресурсами (ссылки на изображения, мультимедийное содержимое, документы и т.д.). Оно определяет# ся на этапе разработки программы. WPF предоставляет для загрузки ресурсов несколько API, которые позволяют получить содержимое, ассоциированное с приложением. С помощью системы конфигурирования мы можем получить доступ к состо# янию, которое сохраняется между запусками и может быть изменено во время выполнения. Если же речь о состоянии, которое фиксировано во время разра# ботки, например, о картинках или определении пользовательского интерфейса, то понадобится система управления ресурсами. В простейшем виде для ссылки на ресурс достаточно указать полный путь к файлу или URI со схемой http (рис. 2.7): <!— Window1.xaml —> <Window ... > <StackPanel Orientation=’Horizontal’> <Image Source=’http://blog.simplegeek.com/images/img_1476small.png’ /> <Image Source=’C:\Users\Public\Pictures\Sample Pictures\Autumn Leaves.jpg’ /> </StackPanel> </Window> Глава 2. Приложения 80 Рис. 2.7. Изображения, загруженные путем задания полного пути к файлу или URI Таблица 2.2. Типы ресурсов и способы их использования Действие при построении API Можно задавать в разметке Что делает Content Application. GetContentStream Да Копирует ресурс в каталог приложения. Resource Application. GetResourceStream Да Встраивает ресурсы в общий ресурс приложения. EmbeddedResource Assembly. GetManifest# ResourceStream Нет Встраивает ресурсы в приложение по отдельности. Эта методика прекрасно работает, когда нужно обращаться к чему#то, что га# рантированно находится на машине пользователя или по указанному URL в сети Web. Но в большинстве случаев мы хотим сослаться на файл, находящийся в ка# кой#то папке относительно папки приложения, или на ресурс, непосредственно встроенный в исполняемый файл. Способов добавить изображение (или любой другой ресурс) в приложение хоть отбавляй. В файле проекта MSBuild9 мы можем сконфигурировать ресурс10 как Content, Resource или EmbeddedResource11. Ресурсы типа EmbeddedResource Или с помощью обозревателя решения в Visual Studio. Еще несколько параметров, например ApplicationDefinition, Compile и Page, не имеют отношения к ресурсам. 11 Типы Resource и EmbeddedResource поддерживают локализацию ресурсов. Можно создать сбор) ки со специальными именами, которые будут содержать альтернативные версии ресурсов. Под) робнее см. http://msdn.microsoft.com/library/default.asp?url=/library/en)us/cpguide/html/cpconcre) atingusingresources.asp. 9 10 Ресурсы и конфигурирование 81 включаются при компиляции непосредственно в сборку и описываются в ее ма# нифесте. Ресурсы типа Content игнорируются, если только параметр CopyTo OutputDirectory не равен PreserveNewest или Always. В первом случае ресурс ко# пируется в выходной каталог, структура которого повторяет структуру исходно# го. Ресурсы типа Resource встраиваются в приложение в виде именованного эле# мента в составе сгенерированного файла ресурсов (табл. 2.2). Чтобы лучше во всем этом разобраться, рассмотрим пример. Пусть есть прос# той проект ResourceTest, содержащий файл program.cs и три картинки: content# image.jpg, resource#image.jpg и embedded#image.jpg. <Project DefaultTargets=’Build’ xmlns=’http://schemas.microsoft.com/developer/msbuild/2003’> ... <ItemGroup> <Content Include=’images\contentimage.jpg’> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> <Resource Include=’images\resourceimage.jpg’ /> <EmbeddedResource Include=’images\embeddedimage.jpg’ /> <Compile Include=’program.cs’ /> </ItemGroup> ... </Project> В результате его компиляции с помощью MSBuild будет создана такая струк# тура каталогов: bin\debug\ ResourceTest.application ResourceTest.exe ResourceTest.exe.manifest ResourceTest.pdb bin\debug\images\ content image.jpg В данном случае картинка embedded#image.jpg встроена12 в исполняемый файл ResourceTest.exe, resource#image.jpg помещена во встроенный ресурс Resource Test.g.resources, а content#image.jpg просто скопирована в каталог debug. Во всех трех случаях имя каталога и имя файла картинки сохранены в имени ресурса. На рис. 2.8 показано, что получилось. А теперь самое время поговорить о том, как ресурсы используются в WPF# приложении. 12 Если быть абсолютно точным, то картинка встраивается в секцию ресурсов, которая является частью сборки. Часто такие ресурсы называются манифестированными, поскольку описаны в ма) нифесте сборки. Глава 2. Приложения 82 компиляция изображения Рис. 2.8. Структура приложения с ресурсами разных типов <!— Window1.xaml —> <Window ... > <StackPanel Orientation=’Horizontal’> <!— Так будет работать —> <Image Source=’images/contentimage.jpg’> <Image Source=’images/resourceimage.jpg’> <!— А так нет!! —> <Image Source=’images/embeddedimage.jpg’> </StackPanel> </Window> И на ресурс, описанный в манифесте, и на скопированное содержимое можно сослаться из разметки, просто указав относительный путь. К ресурсам типа EmbeddedResource напрямую из файла разметки обратиться нельзя. Но програм# мно большинство компонентов WPF поддерживают обращение по URI: Image img = new Image(); img.Source = new BitmapImage( new Uri(«images/content image.jpg», UriKind.Relative)); Любой высокоуровневый API в конечном итоге обращается к API работы с ре# сурсами, реализованном в классе Application. С помощью относительных (rela# tive) URI можно получить доступ к ресурсам, ассоциированным с приложением: public class Application : DispatcherObject, IResourceHost { ... public static StreamResourceInfo GetContentStream(Uri uriContent); Ресурсы и конфигурирование 83 public static StreamResourceInfo GetRemoteStream(Uri uriRemote); public static StreamResourceInfo GetResourceStream(Uri uriResource); ... } Три API для доступа к ресурсам из приложения соответствуют трем разным логическим хранилищам: • GetContentStream служит для доступа к ресурсам, заданным в файле проекта с помощью параметра Content. Эти ресурсы представлены на диске в виде отдельных файлов. • GetResourceStream применяется для доступа к ресурсам, встроенным в исполняемый файл, то есть тем, которые описаны с помощью параметра Resource. • GetRemoteStream можно использовать для доступа к произвольному содержимому, находящемуся в исходном узле приложения. Исходный узел – это каталог или Web#сервер, откуда было запущено приложения. Обычно этот метод применяется для очень объемных ресурсов или таких, которые логически отделены от самого приложения. Открытое соглашение о пакетах И содержимое, и ресурсы пользуются единым механизмом пакетов. Дополни) тельную информацию о нем дает открытое соглашение о пакетах (Open Packaging Conventions или OPC). В мире COM было понятие о структурированном хранили) ще, для работы с которым был определен ряд интерфейсов (самые важные – IStorage и IStream). В .NET 2.0 не было прямого аналога этой технологии. По сути своей, структурированное хранилище обеспечивало единообразный доступ к структурированной файловой системе. Его интерфейсы могли быть реализованы поверх любой модели упаковки, самой популярной из которых был двоичный фор) мат файла, известный под названием составные файлы OLE (OLE compound files). Именно этот формат повсеместно применялся в Microsoft Office. .NET 3.0 включает механизм, являющийся логическим продолжением структу) рированного хранилища и формата составных файлов OLE. Точно так же, как ин) терфейсы IStorage и IStream могли быть реализованы на базе любого формата упаковки, так и в пространстве имен System.IO.Packaging определены интерфей) сы и типы, которые можно применить для доступа к любому формату. И, если сос) тавные файлы OLE являлись эталонной реализацией структурированного храни) лища, то ZipPackage дает эталонную реализацию OPC)пакета (используется алго) ритм сжатия ZIP и метаданные в формате XML). Об OPC достаточно знать, что это абстрактный способ доступа к структуриро) ванной файловой системе, не зависящий от модели упаковки. Все ссылки на ре) сурсы в WPF – это, по существу, ссылки на части OPC. В модели OPC можно выделить три основных концепции: пакет, часть и связь. Один пакет состоит из нескольких частей, между которыми имеются связи. Связи закодированы в части со специальным именем. Эта модель хорошо документиро) вана в спецификации OPC по адресу www.microsoft.com/whdc/xps/downloads.mspx. 84 Глава 2. Приложения Помимо средств загрузки ресурсов, объект Application предоставляет прямой доступ к загрузке компонентов, определенных в разметке: public class Application : DispatcherObject, IResourceHost { ... public static object LoadComponent(Uri resourceLocator); public static void LoadComponent(object component, Uri resourceLocator); ... } Эти два перегруженных варианта метода LoadComponent применяются для заг# рузки и разбора компонентов, написанных на XAML. Первый обращается к методу GetResourceStream и загружает встроенный XAML#ресурс в объект. Наверное, чаще всего он используется для загрузки части описания пользовательского интерфейса. У любого компонента, написанного на XAML, есть два имени: URI и имя CLR# типа. Когда объект типа, определенного с помощью разметки, создается с помощью ключевого слова new, конструктор этого типа вызывает метод InitializeComponent, который в свою очередь загружает код на языке XAML. До этого места метод InitializeComponent представлялся какой#то магией. Давайте потратим некоторое время, чтобы познакомиться с тем, как он работает на самом деле. Рис. 2.9. Простой пример: окно с одной кнопкой Начнем с определения очень простого пользовательского интерфейса, состоя# щего из окна с одной кнопкой. При запуске это приложение выглядит, как пока# зано на рис. 2.9. <!— LoadTest.xaml —> <Window x:Class=’EssentialWPF.LoadTest’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’EssentialWPF’ > <Button>Hello World</Button> </Window> Ресурсы и конфигурирование 85 // LoadTest.xaml.cs public partial class LoadTest : Window { public LoadTest() { InitializeComponent(); } } Что произойдет, если закомментировать вызов InitializeComponent? Попро# буйте, я подожду. Окно оказалось пустым, правильно? Проблема в том, что внутри Initialize Component вызывается метод LoadComponent, который загружает XAML#до# кумент и генерирует визуальные элементы. Можно сделать это и самостоя# тельно: public partial class LoadTest : Window { public LoadTest() { Application.LoadComponent(this, new Uri(«LoadTest.xaml», UriKind.Relative)); } } Метод InitializeComponent также присваивает значения некоторым перемен# ным#членам и подписывается на события, поэтому в общем случае я не рекомен# дую опускать его вызов. Но понимать, что он делает, тем не менее полезно. Второй вариант метода LoadComponent идентичен вызову new для типа CLR. Оба варианта очень сильно различаются, поэтому давать им одинаковые имена было ошибкой. Следующие строки дают в точности один и тот же ре# зультат: LoadTest load; load = (LoadTest)Application.LoadComponent( new Uri(«LoadTest.xaml», UriKind.Relative)); load = new LoadTest(); Здесь четко видны оба имени LoadTest: URI и тип CLR. Состояние)документ Состояние#документ – это набор данных, ассоциированных с пользовательс# ким документом (например, документом Microsoft Word или графическим фай# лом). Хотя в .NET есть службы для создания документов и манипулирования ими, WPF не предоставляет никакого стандартного каркаса для управления сос# тоянием#документом. Эта обязанность возлагается на автора приложения. Теперь, когда мы познакомились с объектом Application, можно попробовать нарисовать на экране пиксель#другой. На верхнем уровне приложения располо# жены строительные блоки трех видов: окна, пользовательские элементы управле# ния и страницы. Глава 2. Приложения 86 Окна В библиотеке User32 оно называлось HWND, в Microsoft Foundation Classes (MFC) – CWnd, а в Windows Forms – Form. Но как ни называй, а в большинстве систем для построения пользовательских интерфейсов необходим визуальный элемент верхнего уровня, которым могут манипулировать конечные пользовате# ли. В WPF такой элемент называется Window. Базовым типом всех окон в WPF является System.Windows.Window. Обычно тип Window применяется для SDI#окон и диалогов. По существу, Window – это просто элемент управления, предназначенный служить контейнером для содер# жимого верхнего уровня. Как правило, окно определяется с помощью комбина# ции разметки и кода: <!— Window1.xaml —> <Window ... x:Class=’EssentialWPF.Window1’ Visibility=’Visible’ Title=’This is a Window!’ > </Window> // Window1.cs namespace EssentialWPF { public partial class Window1 : Window { public Window1() { InitializeComponent(); } } } При запуске этой программы появится ничем не примечательное окно, изоб# раженное на рис. 2.10. Отметим, что в жизни даже такого простого окна есть много этапов. Вот самые важные из них: Вызывается конструктор. Возбуждается событие Window.Initialized. Возбуждается событие Window.Activated13. Возбуждается событие Window.Loaded. Возбуждается событие Window.ContentRendered. Пользователь взаимодействует с окном. Возбуждается событие Window.Closing. Возбуждается событие Window.Unloaded. Возбуждается событие Window.Closed. 13 Есть еще событие Windows.Deactivated, которое возбуждается всякий раз, как окно теряет фокус. Обычно это происходит на этапе 6. Окна 87 События Activated и Deactivated могут возникать много раз на протяжении жизни окна, если пользователь переключается с одного окна на другое. Событие ContentRendered возникает после того, как окно полностью нарисовано. Чтобы выполнить какое#то действие еще до того, как на экране что#то отобразилось, мы пользуемся событием Loaded, а если нужно что#то сделать сразу после того, как окно было явлено взору пользователя, – событием ContentRendered. Рис. 2.10. Малоинтересное окно Наверное, чаще всего используются события Loaded, Closing и Closed. В обра# ботчике события Loaded обычно инициализируется состояние окна. Ниже мы за# даем заголовок окна; при запуске программы видно, что заголовок устанавлива# ется еще до того, как окно появляется на экране: <!— Window1.xaml —> <Window ... Loaded=’WindowLoaded’ > </Window> // Window1.xaml.cs ... void WindowLoaded(object sender, RoutedEventArgs e) { Title = «This window was loaded at « + DateTime.Now; } ... Событие Closing возникает перед тем, как закрыть окно. Это событие можно отменить и тем самым не дать окну закрыться. Часто это используют, чтобы зап# росить у пользователя подтверждение: <!— Window1.xaml —> <Window ... Closing=’WindowClosing’ > </Window> // Window1.xaml.cs ... 88 Глава 2. Приложения void WindowClosing(object sender, CancelEventArgs e) { if (MessageBox.Show(«Вы уверены, что хотите закрыть окно?», «Подтверждение», MessageBoxButton.YesNo) == MessageBoxResult.No) { // воспрепятствовать закрытию окна e.Cancel = true; } else { // окно будет закрыто } } ... Это окно закроется только, если пользователь ответит Да в окне сообщения. Наконец, событие Closed возникает, когда окно уже закрыто (не видно поль# зователю). Пример я приводить не стану, так как это событие используется ред# ко. Если попытаться сохранить измененный пользователем документ в обработ# чике события Closed, то что произойдет в случае ошибки записи файла? Окно#то уже закрыто. Лучше сохранять работу в обработчике Closing, тогда в случае воз# никновения ошибки можно будет отменить закрытие окна. Отображение окна Существует три основных способа вывести окно на экран: методы Show и ShowDialog, и свойство Visibility. Вызов метода Show и установка свойства Visibility в true приводят к одному и тому же результату: выводу немодально# го окна14. Из программы окно обычно открывается методом Show, но в случае привязки к данным или декларативного программирования свойство удобнее, поэтому в WPF есть и то, и другое. Немодальные окна лучше всего подходят для реализации оконной модели SDI. Чтобы вывести модальное диалоговое окно, мы вызываем метод ShowDialog. С помощью модальных окон реализуется оконная модель на базе диалогов: <!— Window1.xaml —> <Window ... Title=’Starter Window’ > <StackPanel> <Button Click=’ShowMethod’>Show</Button> <Button Click=’UseVisibilityProperty’>Visibility = Visible</Button> <Button Click=’ShowDialogMethod’>ShowDialog</Button> </StackPanel> </Window> 14 Странным словом модальность описывается поведение окна относительно других окон приложе) ния. Окно, которое блокирует все остальные окна, называется модальным. Чтобы пользователь мог продолжить взаимодействие с другими окнами, он должен сначала закрыть модальное окно. Немодальные окна ведут себя прямо противоположным образом. Они не блокируют другие окна, поэтому пользователь может взаимодействовать с произвольным числом присутствующих на эк) ране немодальных окон. Окна 89 // Window1.xaml.cs ... void ShowMethod(object sender, RoutedEventArgs e) { Window w = new Window(); w.Title = «Show»; w.Show(); } void UseVisibilityProperty(object sender, RoutedEventArgs e) { Window w = new Window(); w.Title = «Visibility = Visible»; w.Visibility = Visibility.Visible; } void ShowDialogMethod(object sender, RoutedEventArgs e) { Window w = new Window(); w.Title = «ShowDialog»; w.Owner = this; w.WindowStartupLocation = WindowStartupLocation.CenterOwner; w.ShowInTaskbar = false; w.ShowDialog(); } ... Нажатие любой из первых двух кнопок дает один и тот же результат, в част# ности, можно сделать активным исходное окно и создать любое число новых окон. Если же нажать третью кнопку, то выбрать исходное окно уже не получит# ся (рис. 2.11). Задача модальных диалогов – не дать пользователю перейти в дру# гое окно, хотя такое поведение часто раздражает, потому что человек хочет сам командовать приложением, а не подчиняться его командам! Обычно модальные диалоги применяются лишь в ситуациях, когда без модальности просто не обой# тись (да, я знаю, что в этой фразе есть циклическая зависимость). Помимо описанных обычных способов отображения окна, хотелось бы уметь соз# давать окно, которое перекрывает своего родителя (плавает поверх него), но не меша# ет сделать родительское окно активным. В общем случае этот механизм полезен для немодальных диалогов и плавающих панелей инструментов. Чтобы реализовать такое окно, мы можем воспользоваться методом Show, одновременно задав свойство Owner (рис. 2.12). Окна, для которых задан владелец, всегда отображаются поверх своего владельца, но при этом не мешают пользователю делать окно владельца активным. <!— Window1.xaml —> <Window ... Title=’Starter Window’ > <StackPanel> <Button Click=’ShowOwner’>Show Owner</Button> <Button Click=’ShowMethod’>Show</Button> <Button Click=’UseVisibilityProperty’>Visibility = Visible</Button> <Button Click=’ShowDialogMethod’>ShowDialog</Button> </StackPanel> </Window> // Window1.xaml.cs Глава 2. Приложения 90 ... void ShowOwner(object sender, RoutedEventArgs e) { Window w = new Window(); w.Owner = this; w.Title = «Show Owner»; w.Show(); } ... Рис. 2.11. Отображение окна различными способами Рис. 2.12. Отображение окна с заданием свойства Owner Окна 91 У окон, имеющих владельца, есть еще ряд особенностей. Они не появляются в списке окон, который отображается при нажатии комбинации клавиш Alt+Tab, хо# тя могут показываться на панели задач. Кроме того, они автоматически закрывают# ся при закрытии окна#владельца и скрываются, когда окно#владелец свертывается. Задание размера и положения Разобравшись с тем, как отобразить окно, посмотрим, как управлять его поло# жением на экране. Существует три основных способа задать размер и положение окна: начальное поведение, взаимодействие с пользователем и текущие значения. У начального поведения есть два аспекта: в каком месте экрана расположить ок# но и какой размер для него выбрать. Начальное положение определяется свойством WindowStartupLocation в сочетании со свойствами Top и Left. Размер же контроли# руется свойствами SizeToContent, Width и Height. Свойство SizeToContent прини# мается во внимание только, если и Width, и Height равны Double.NaN (автомати# ческий выбор), и пользователь не изменил размер окна. После того как желаемое положение и размеры окна заданы, что может делать с окном пользователь? Возможности взаимодействия определяются свойством ResizeMode, от него зависит, может ли пользователь вообще изменять размер ок# на. Кроме того, свойства MinWidth, MaxWidth, MinHeight и MaxHeight опреде# ляют предельные ширину и высоту окна. Так, в следующем примере окно нельзя сделать уже 500 пикселей, поэтому содержимое всегда можно будет прочесть. <Window ... Title=’EssentialWPF’ MinWidth=’500’ > ... </Window> Наконец, получить или изменить текущие значения положения и размера ок# на можно с помощью свойств Width, Height, Top и Left. Изменение размера окна тесно связано с размещением элементов внутри него, но эту тему мы отложим до главы 4. Пользователи ожидают от окон конкретного стиля определенного поведения: размера, начального положения и наличия владельца. Так, подразумевается, что диалоговое окно отображается с помощью примерно такого кода: void ShowDialogMethod(object sender, RoutedEventArgs e) { Window w = new Window(); w.Title = «ShowDialog»; w.Owner = this; w.SizeToContent = SizeToContent.WidthAndHeight; w.WindowStartupLocation = WindowStartupLocation.CenterOwner; w.ShowInTaskbar = false; w.ShowDialog(); } Глава 2. Приложения 92 Здесь говорится, что окно центрируется относительно своего владельца, не показывается на панели задач и автоматически подстраивает размер под содер# жимое. Объекты Window и Application После того как окно (или несколько окон) показаны на экране, нам часто бы# вает нужно найти все открытые окна. Как правило, приложения с несколькими окнами верхнего уровня (например, Microsoft Office Word 2003) содержат пункт меню Window, позволяющий переходить от одного открытого докумен# та к другому. Реализовать это можно было бы, заведя глобальную переменную# список, в который окно добавляется в момент создания и удаляется при закры# тии. Однако команда WPF решила встроить эту возможность в объект Application, которому автоматически посылается уведомление о создании и закрытии окна. Таблица 2.3. Поведение при завершении приложения ShutdownMode Поведение Типичное применение OnLastWindowClose (по умолчанию) Приложение завершается, когда открытых окон не осталось. Каждое созданное окно добавляется в список и удаляется из него при закрытии. SDI#приложения с несколькими окнами верхнего уровня, например, Microsoft Internet Explorer. OnMainWindowClose Приложение завершается, когда закрывается окно, присвоенное свойству MainWindow приложения. SDI#приложения с главным окном, например, приложение с одним окном документа и набором плавающих окон с инструментами (как Adobe Photoshop). OnExplicitShutdown Любой сценарий, в котором реализована нестандартная логика завершения приложения. Например, в приложении может вообще не быть открытых окон, а его присутствие обозначается значком в системном лотке (как в случае MSN Messenger). Приложение завершается только после того, как в программе явно вызван метод ShutDown. Поскольку объекту Application посылаются уведомления обо всех окнах, он может сам завершить приложение после закрытия последнего окна (или по дру# гим причинам). Иногда метод Shutdown вызывается автоматически в зависимос# ти от значения свойства ShutdownMode объекта Application. В табл. 2.3 перечис# лены возможные значения этого свойства. Пользовательские элементы управления 93 Перебрать все открытые в данный момент окна позволяет свойство Application.Windows. Для иллюстрации создадим окно с двумя кнопками. Пер# вая кнопка создает новое окно, вторая выводит список всех открытых окон: <!— WindowList.xaml —> <Window ... x:Class=’EssentialWPF.WindowList’ Title=’Window List’ SizeToContent=’WidthAndHeight’ > <StackPanel> <Button Click=’NewWindowClicked’>Create New Window</Button> <Button Click=’ListOpenWindows’>List Open Windows</Button> </StackPanel> </Window> Чтобы не было сомнений в том, что мы создаем несколько окон, заведем ста# тическую переменную и будем увеличивать ее на единицу, так чтобы заголовки всех окон различались. В обработчике события NewWindowClicked мы можем просто создать и показать новое окно: // WindowList.xaml.cs public partial class WindowList : Window { static int _createCount; public WindowList() { InitializeComponent(); Title = «Window List « + _createCount; _createCount++; } void NewWindowClicked(object sender, RoutedEventArgs e) { new WindowList().Show(); } } А в обработчике события ListOpenWindows мы можем обойти весь набор окон в объекте Application и построить строку, содержащую их заголовки. При запус# ке программа ведет себя, как показано на рис. 2.13: void ListOpenWindows(object sender, RoutedEventArgs e) { StringBuilder sb = new StringBuilder(); foreach (Window openWindow in Application.Current.Windows) { sb.AppendLine(openWindow.Title); } MessageBox.Show(sb.ToString(), «Open Windows»); } Пользовательские элементы управления В предыдущем разделе мы узнали, как создавать окна. Интересно отметить, что окно – это способ инкапсуляции определенной функциональности. Кроме того, окна изолированы, потому что, во#первых, для них определен новый тип Глава 2. Приложения 94 CLR, а, во#вторых, потому что окна верхнего уровня изолированы визуально. Часто возникает желание разбить описание пользовательского интерфейса на мелкие инкапсулированные части, которые не обязательно представляют собой отдельные окна. Здесь#то и пригодятся пользовательские элементы управления. Рис. 2.13. Вывод списка открытых окон Есть две категории разработчиков элементов управления. Одни создают поль# зовательские элементы (user control), другие – нестандартные (custom control). Названия не очень осмысленные (восходят к временам Visual Basic 5.0, в котором появился простой способ разрабатывать элементы управления), но относятся к двум совершенно разным ситуациям. Я предпочитаю такое определение: пользо# вательские элементы – это способ инкапсуляции частей графического интерфей# са, а нестандартные – это повторно используемые элементы, которые можно при# менять и в других приложениях. Вопрос о том, где проходит граница между ни# ми, – источник ожесточенных споров. Определение пользовательского элемента управления похоже на определение нового окна, только в качестве базового можно использовать любой класс. На практике, скорее, создание окна можно назвать частным случаем создания поль# зовательского элемента. Обычно в качестве базового класса для пользовательс# кого элемента управления берется класс ContentControl: <ContentControl ... x:Class=’EssentialWPF.MyUserControl’ > <Button Click=’ButtonClicked’>Hello World</Button> </ContentControl> Как и в случае окон, всю логику можно поместить в файл с кодом: public partial class MyUserControl : ContentControl { public MyUserControl() { Пользовательские элементы управления 95 InitializeComponent(); } void ButtonClicked(object sender, RoutedEventArgs e) { MessageBox.Show(«Howdy!»); } } Этот новый элемент можно использовать в любом месте приложения, но сна# чала мы должны создать для него пространство имен XML. Язык XAML позво# ляет ассоциировать любое пространство имен CLR с пространством имен XML: <Window ... x:Class=’EssentialWPF.UserControlHost’ xmlns:l=’clr namespace:EssentialWPF’ Title=’User Controls’ > ... </Window> Voilа! Теперь этот пользовательский элемент управления можно поместить в окно (рис. 2.14): <Window ... x:Class=’EssentialWPF.UserControlHost’ xmlns:l=’clr namespace:EssentialWPF’ Title=’User Controls’ > <StackPanel> <l:MyUserControl /> <l:MyUserControl /> </StackPanel> </Window> Рис. 2.14. Окно с двумя экземплярами нового пользовательского элемента управления Глава 2. Приложения 96 Ссылается Рис. 2.15. Логическая модель работы ссылок на пользовательский элемент управления Здесь два нестандартных тега <l:MyUserControl /> ссылаются на компонент, определенный в файле MyUserControl.xaml (рис. 2.15). Если изменить его опре# деление, то изменятся оба элемента. Назначение пользовательских элементов – разбить приложение на компонен# ты. Если же нужно создать совсем новый элемент (с поддержкой тем и т.д.), то стоит подумать о нестандартных элементах. Для создания таких элементов нуж# но хорошо понимать модель элементов управления в WPF, а это уже выходит за рамки настоящей книги. Итак, мы видели, что создание окна – частный случай создания пользова# тельского элемента управления. Еще один способ разбить приложение на обозри# мые и удобные для сопровождения части – воспользоваться страницами, кото# рые являются частью инфраструктуры навигации в WPF. Навигация и страницы Часто языку HTML приписывают появление навигации в приложениях, но на самом деле она гораздо старше. Любой мастер, программа gopher или электрон# ная доска объявлений – вот ранние примеры навигации. HTML лишь сделал идею навигации популярной, реализовав последователь# ности страниц, связанных гиперссылками. В силу повсеместного распростране# ния Web практически любой пользователь знает, как пользоваться ссылками для навигации по информационному пространству или приложению. Чтобы было проще создавать приложения с такой моделью навигации, в WPF встроен готовый каркас. Его основные компоненты: навигационный узел Навигация и страницы 97 (NavigationWindow), содержимое (Page) и журнал (рис. 2.16)15. Задача журнала – вести учет действий, имеющих отношений к навигации, но в каркасе ему не соот# ветствует никакой открытый класс. NavigationWindow – это принимаемый по умолчанию навигационный узел в WPF. Этот класс является производным от Window и добавляет стандартный интерфейс навигации (кнопку Назад и т.д.), а также необходимую для навигации инфраструктуру. Класс NavigationWindow имеет доступ к тем же самым средствам уровня приложения, что и Window. С его помощью мы можем реализовать навигацию в однодокументной оконной модели. навигационный узел NavigationWindow или браузер содержимое Page Journal BackStack, ForwardStack Рис. 2.16. Логическая модель навигации В WPF целью навигации может быть любое содержимое: объекты данных, примитивные типы (например, строки) или страницы. Page – не более чем удоб# ный класс, содержащий вспомогательные методы, облегчающие навигацию. Прежде чем переходить к полноценному решению на базе разметки, рассмотрим простой пример навигации, реализованный целиком программно, чтобы понять, как стыкуются все компоненты. Для начала создадим новое окно. Поскольку мы хотим поддержать навигацию, унаследуем класс окна от NavigationWindow, а не от Window: 15 Не следует путать историю с журналом. В Internet Explorer эти две концепции называются соответ) ственно History и TravelLog. History — это сохраняемый перечень посещенных сайтов, что)то вроде автоматически формируемого списка закладок. Журнал, или TravelLog ) это список сайтов, посе) щенных на протяжении данного сеанса. Разница тонкая, но важная в контексте каркаса навигации. 98 Глава 2. Приложения public class NavExample : NavigationWindow { public NavExample() { } } Объект типа NavigationWindow – это навигационный узел, то есть в нем может размещаться содержимое, на которое можно перейти с помощью навигации. Это со# держимое будет представлено классом, производным от Page. Новая страница созда# ется примерно так же, как объект Window или пользовательский элемент управления: public class Page1 : Page { public Page1() { this.WindowTitle = «Page 1»; } } Теперь нужно сказать объекту NavigationWindow, чтобы он отобразил первую страницу (Page1): public class NavExample : NavigationWindow { public NavExample() { Navigate(new Page1()); } } Фантастика! Мы создали навигационный узел, в котором отображается содер# жимое. Однако пока это не очень интересно, так как пользователю некуда пере# ходить. В теперешнем виде программа отображает лишь пустое окно. Определим вторую страницу: public class Page2 : Page { public Page2() { WindowTitle = «Page 2»; } } Добавим еще на первую страницу какое#нибудь содержимое, в том числе ги# перссылку16: public class Page1 : Page { public Page1() { TextBlock block = new TextBlock(); Hyperlink link = new Hyperlink(); link.Inlines.Add(«Click for page 2»); block.Inlines.Add(link); Content = block; WindowTitle = «Page 1»; } } 16 Смысл этого кода будет подробно описан в главе 5. Пока поверьте на слово, что здесь в гиперс) сылку просто добавляется текст. Навигация и страницы 99 Последний шаг – обработать событие Click элемента Hyperlink, чтобы перей# ти на следующую страницу. Объект NavigationService предоставляет не завися# щий от узла доступ к операциям навигирования. Для него не важно, размещена ли страница в браузере или в узле типа NavigationWindow: public class Page1 : Page { public Page1() { TextBlock block = new TextBlock(); Hyperlink link = new Hyperlink(); link.Click += LinkClicked; link.Inlines.Add(«Click for page 2»); block.Inlines.Add(link); Content = block; WindowTitle = «Page 1»; } void LinkClicked(object sender, RoutedEventArgs e) { NavigationService.Navigate(new Page2()); } } При запуске этой программы будет выведено окно, показанное на рис. 2.17. Если щелкнуть по ссылке, то мы перейдем на вторую страницу. Сама она пуста, но текст в заголовке окна изменился (рис. 2.18). Рис. 2.17. Начальный вид окна навигации Вот теперь все основные компоненты навигации на месте. У нас есть узел, со# держимое и журнал, который отслеживает все операции и предоставляет неслож# ный интерфейс для перемещения вперед и назад. После щелчка по ссылке стано# вится доступна кнопка со стрелкой влево, которая возвращает пользователя на предыдущую страницу, после чего активируется кнопка со стрелкой вправо. Глава 2. Приложения 100 Рис. 2.18. После перехода на вторую страницу Все это возможно благодаря журналу. Объект NavigationWindow предостав# ляет стандартный интерфейс для навигации (кнопку Назад, полосу заголовка и т.д.) и автоматически создает журнал. Эквивалентная этому примеру разметка по идее должна выглядеть как хоро# шо знакомый HTML#код. Мы можем воспользоваться тем фактом, что класс Hyperlink уже поддерживает навигацию на основе URI. Это позволит заменить весь написанный выше код следующей разметкой: <!— page1.xaml —> <Page ... WindowTitle=’Page 1’> <TextBlock> <Hyperlink NavigateUri=’page2.xaml’> Click for page 2 </Hyperlink> </TextBlock> </Page> <!— page2.xaml —> <Page ... WindowTitle=’Page 2’> </Page> <!— navexample.xaml —> <NavigationWindow ... x:Class=’EssentialWPF.NavExample’ Source=’page1.xaml’> </NavigationWindow> Навигация и страницы 101 Свойству Source объекта NavigationWindow можно присвоить URI#имя пер# вой страницы Page1.xaml, чтобы обойтись без создания экземпляра типа. Анало# гично если свойству NavigateUri объекта Hyperlink присвоить URI#имя второй страницы Page2.xaml, то не придется писать код обработки события Click. Если, как часто бывает, мы не хотим менять стандартный вид окна навигации, то можно не определять его вовсе. Присвоив значение свойству StartupUri объек# та Application, мы сможем сразу запустить страницу: <!— app.xaml —> <Application ... x:Class=’EssentialWPF.App’ StartupUri=’page1.xaml’> </Application> Передача состояния между страницами Все это замечательно, если у нас есть всего одна страница или вторая страни# ца ничего не делает. Но, чтобы двигаться дальше, надо научиться передавать дан# ные от одной страницы другой. В HTML передача данных обычно происходит за счет сохранения состояния на стороне сервера или в куке на стороне клиента. В WPF для передачи данных можно воспользоваться словарем, хранящимся в свойстве Properties объекта Application. Для следующего примера напишем две страницы: первая будет запрашивать имя пользователя, а вторая – выводить ему приветствие. На первой странице по# местим текстовое поле для ввода данных и ссылку для перехода на следующую страницу: <Page ... x:Class=’EssentialWPF.Name’ WindowTitle=’Who are you?’ > <StackPanel> <Label>What is your name?</Label> <TextBox Name=’_nameBox’/> <TextBlock> <Hyperlink NavigateUri=’hello.xaml’>Continue</Hyperlink> </TextBlock> </StackPanel> </Page> На сторой странице будет только метка, в которой отображается введен# ное имя: <Page ... x:Class=’EssentialWPF.Hello’ WindowTitle=’Hello!’ > <StackPanel> <TextBlock>Hello!</TextBlock> Глава 2. Приложения 102 <Label Name=’_name’ /> </StackPanel> </Page> Первый шаг – скопировать данные из текстового поля на страницу с именем и заставить приложение показать эту страницу. Затем можно модифицировать логику навигации, добавив сохранение необходимых данных. Подписавшись на событие RequestNavigate, мы сможем сохранить имя в объекте Application непос# редственно перед тем, как произойдет переход (рис. 2.19): Рис. 2.19. Ввод данных на странице name.xaml <!— name.xaml —> <Page ... x:Class=’EssentialWPF.Name’ WindowTitle=’Who are you?’ > <StackPanel> ... <Hyperlink NavigateUri=’hello.xaml’ RequestNavigate=’Page_RequestNavigate’ > Continue </Hyperlink> ... </StackPanel> </Page> Навигация и страницы 103 // name.xaml.cs public partial class Name : Page { ... void Page_RequestNavigate(object sender, RequestNavigateEventArgs e) { Application.Current.Properties[«Name»] = _nameBox.Text; } } Рис. 2.20. Получение данных от страницы name.xaml На второй странице необходимо подписаться на событие Loaded, извлечь данные из объекта Application и вывести их (рис. 2.20): <!— hello.xaml —> <Page ... x:Class=»EssentialWPF.Hello» Text=»Hello!» Loaded=»PageLoaded» > ... </Page> // hello.xaml.cs using System; using System.Windows; using System.Windows.Navigation; using System.Windows.Controls; Глава 2. Приложения 104 namespace EssentialWPF { public partial class Hello : Page { public Hello() { InitializeComponent(); } void PageLoaded(object sender, EventArgs e) { _name.Content = Application.Current.Properties[«Name»]; } } } Такой подход прекрасно работает в простых сценариях, но вообще#то исполь# зование объекта Application подобным образом ничем не отличается от использо# вания глобальной переменной. Вся моя объектно#ориентированная природа воз# мущается, когда я вижу код с глобальными переменными. Напомню, что мы не обязаны выполнять навигацию только с помощью URI; можно создать вторую страницу программно и задать для нее свойства, как для любого объекта. Тогда обмен данными между двумя страницами удастся инкапсулировать: <!— hello.xaml —> <Page ... x:Class=’EssentialWPF.Hello’ Text=’Hello!’ > ... </Page> // hello.xaml.cs ... public partial class Hello : Page { public string Name { set { _name.Content = value; } } } ... <!— name.xaml —> <Page ...> ... <Hyperlink Click=’Navigate’>Continue</Hyperlink> ... </Page> // name.xaml.cs ... void Navigate(object sender, RoutedEventArgs e) { Hello hello = new Hello(); hello.Name = _nameBox.Text; NavigationService.Navigate(hello); } ... Навигация и страницы 105 Многообещающий способ передачи состояния между страницами – аргумент navigationState метода Navigate: public class NavigationService { ... public bool Navigate(object root); public bool Navigate(Uri source); public bool Navigate(object root, object navigationState); public bool Navigate(Uri source, object navigationState); ... } Хотя догадаться об этом довольно сложно, мы можем запомнить данные в обработ# чике события LoadCompleted объекта NavigationService. Сначала изменим вызываю# щую программу так, чтобы она передавала дополнительные данные при вызове Navigate: void Navigate(object sender, RoutedEventArgs e) { Hello hello = new Hello(); NavigationService.Navigate(hello, _nameBox.Text); } На второй странице подпишемся на событие LoadCompleted. Свойство стра# ницы NavigationService равно null, пока не произойдет событие Loaded, поэтому мы должны подписаться на событие LoadCompleted17 в обработчике события Loaded (я же сказал, что догадаться нелегко): void PageLoaded(object sender, EventArgs e) { NavigationService.LoadCompleted += LoadCompleted; } void LoadCompleted(object sender, NavigationEventArgs e) { _name.Content = e.ExtraData; NavigationService.LoadCompleted — = LoadCompleted; } Существует много способов подписаться на событие LoadCompleted, и у каждого есть свои плюсы и минусы. Использование объекта Application означает наличие некоего гло# бального состояния, с которым трудно работать в крупных приложениях. Задание свойств отдельных страниц лишает нас возможности декларативно описывать навигацию на язы# ке XAML. У аргумента navigationState метода Navigate такая же проблема. Мне кажется, что для страниц с параметрами лучше всего создать команду нави# гации (поскольку команды могут иметь параметры) и стилизовать кнопку (которая поддерживает команды) под гиперссылку18. Не особенно элегантно, но работает. Отметим также, что в том же обработчике необходимо отказаться от подписки на событие LoadCompleted. Дело в том, что LoadCompleted — событие объекта NavigationService, поэтому оно будет возникать для каждой страницы, загружаемой в NavigationService. Отказываясь от подписки после получения извещения от загружаемой страницы, мы избегаем получения лишних извещений. Кроме того, отказ от подписки важен и для того, чтобы объект NavigationService не держал нашу страницу в памяти в течение всего времени жизни службы. 18 Пример см. на странице http://blog.simplegeek.com/book. 17 106 Глава 2. Приложения Управление навигацией В предыдущем примере есть одна проблема: пользователь может покинуть первую страницу, не введя имя. При работе с объектом Window мы можем вос# пользоваться событием Closing (или любым другим способом выявления попыт# ки закрытия окна), чтобы не дать пользователю закрыть диалоговое окно, не за# полнив обязательные поля. Эквивалентная функциональность имеется в классе NavigationService: public class NavigationService { ... public event LoadCompletedEventHandler LoadCompleted; public event NavigatedEventHandler Navigated; public event NavigatingCancelEventHandler Navigating; public event NavigationProgressEventHandler NavigationProgress; public event NavigationStoppedEventHandler NavigationStopped; ... } Событие Navigating позволяет отменить переход точно так же, как отменяет# ся закрытие окна. Поскольку в классе Page нет такого же простого события, как Window.Closing, придется немного потрудиться. Причина довольно тонкая, но существенная. Когда мы подписываемся на событие Navigating, мы сохраняем указатель на наш объект в объекте NavigationService, являющемся частью нави# гационного узла. Если не разорвать эту связь, то страница будет храниться в па# мяти вечно. Тут возникает небольшая сложность; поскольку по завершении на# вигации доступа к объекту NavigationService уже нет, ссылку на него необходи# мо запомнить. Рассмотрим код для реализации контроля на нашей странице. Первым делом подпишемся на событие Loaded объекта Page. Это событие будет возникать при каждом переходе на данную страницу: <!— name.xaml —> <Page ... x:Class=’EssentialWPF.Name’ Text=’Who are you?’ Loaded=’PageLoaded’ > ... </Page> В обработчике события Loaded нужно сделать две вещи: подписаться на инте# ресующие нас события и кэшировать объект NavigationService: NavigationService _navigation; void PageLoaded(object sender, EventArgs e) { _navigation = this.NavigationService; Навигация и страницы 107 _navigation.Navigating += Page_Navigating; _navigation.Navigated += Page_Navigated; } Мы подписались на событие Navigated просто для того, чтобы вовремя отказать# ся от подписки на другие события и обнулить ссылки во избежание утечек памяти: void Page_Navigated(object sender, NavigationEventArgs e) { _navigation.Navigating = Page_Navigating; _navigation.Navigated = Page_Navigated; _navigation = null; } И, наконец, перейдем к обработчику события Navigating, в котором и произво# дится контроль: void Page_Navigating(object sender, NavigatingCancelEventArgs e) { if (_nameBox.Text.Length == 0) { e.Cancel = true; } } Это код не даст покинуть страницу, пока не будет введено имя. Запустив прог# рамму, попробуйте щелкнуть по кнопке Вперед в окне навигации после того, как нажмете кнопку Назад на странице «Hello». После возврата назад поле имени окажется пустым, и, следовательно, попытка перейти вперед не увенчается успе# хом. Поскольку мы хотим перейти на ту страницу, где уже были, следовало бы отключить контроль. Небольшая модификация позволит распознать этот случай: void Page_Navigating(object sender, NavigatingCancelEventArgs e) { if (e.NavigationMode == NavigationMode.New && _nameBox.Text.Length == 0) { e.Cancel = true; } } Управление журналом Итак, мы научились управлять навигацией; мы можем указать, когда и куда следует переходить. Следующая задача, с которой часто сталкиваются разработ# чики приложений со средствами навигации, – это управление журналом. Пос# кольку журнал следит за тем, где пользователь побывал, и отвечает за состояние кнопок Вперед и Назад, есть много ситуаций, в которых им желательно управ# лять. Быть может, самые распространенные – это корзина для покупок (после оформления заказа журнал следует очистить, чтобы исключить случайную пов# торную отправку того же заказа) и страница регистрации (после того как пользо# ватель зарегистрировался, кнопка Назад должна вести на начальную страницу, а не снова на страницу регистрации) (рис. 2.21). Глава 2. Приложения 108 Чтобы понять, как управлять журналом, изменим предыдущий пример, вклю# чив в него начальную страницу: <!— home.xaml —> <Page ... x:Class=’EssentialWPF.Home’ WindowTitle=’Home’ > <TextBlock> Welcome to the application. I can say <Hyperlink NavigateUri=’name.xaml’>hello</Hyperlink> to you. </TextBlock> </Page> <!— app.xaml —> <Application ... x:Class=’EssentialWPF.App’ StartupUri=’home.xaml’ > </Application> навигационный узел навигационный узел содержимое навигационный узел содержимое содержимое Назад Начальная Журнал Переход Имя home.xaml Переход Журнал Hello home.xaml Журнал Рис. 2.21. Так должны вести себя элементы навигации после того, как пользователь зарегистрировался Далее нужно модифицировать страницу регистрации (name.xaml), так чтобы она удаляла себя из журнала после успешного получения имени пользователя. Весь API для работы с журналом находится в классе NavigationService: public sealed class NavigationService { public bool CanGoBack { get; } public bool CanGoForward { get; } public void AddBackEntry(CustomContentState state); public void GoBack(); public void GoForward(); public JournalEntry RemoveBackEntry(); } На рис. 2.21 ясно видно, что мы не хотим помещать страницу name.xaml в жур# нал, поскольку кнопка Назад должна вести со страницы «Hello» на начальную Навигация и страницы 109 страницу. Для удаления записи из журнала достаточно вызвать метод RemoveBackEntry по завершении навигации: void Page_Navigated(object sender, NavigationEventArgs e) { _navigation.RemoveBackEntry(); ... } Помимо управления журналом, мы можем еще и опрашивать его. Чаще всего это бывает необходимо для помещения на страницу кнопки Назад в нужный мо# мент времени. Задача решается тривиально: свойство CanGoBack позволяет ак# тивировать или деактивировать кнопку, а метод GoBack выполняет саму опера# цию перехода. Функциональная навигация и страничные функции В наши дни традиционная навигация не структурирована. Это как програм# мирование на языке BASIC в 1960#е годы – использование goto для произволь# ных переходов между разными частями программами, глобальных переменных для отслеживания состояния и необходимость изменять номера строк при любой модификации. Сегодня мы говорим о гиперссылках, состоянии сеанса и URI, но суть проблемы остается той же. Работу Web#сайта легко нарушить, поток управ# ления в нем изменяется с трудом, а для инкапсуляции функциональности прихо# дится затрачивать несоразмерные усилия. Main SayHello GetName Рис. 2.22. Поток управления в консольном приложении Глава 2. Приложения 110 WPF вводит новую, функциональную модель, в которой навигация выглядит, скорее, как вызов функции. Чтобы понять, что я имею в виду, говоря о функцио# нальной навигации, рассмотрим простой пример. Пусть надо написать програм# му, которая приветствует пользователя, запрашивает его имя и говорит «здрав# ствуйте». На рис. 2.22 схематично представлен поток управления в такой прог# рамме. Реализация ее в виде стандартного консольного приложения тривиальна. Конечно, это примитивный пример, но инкапсулировав его функциональность в набор методов, мы потом сможем использовать ее в других частях приложения: // program.cs using System; static class Program { static void Main() { Console.WriteLine(«Вас приветствует моя программа»); SayHello(); } static void SayHello() { string name = GetName(); Console.WriteLine(«Здравствуйте, {0}!», name); } static string GetName() { Console.Write(«Как вас зовут? «); string name = Console.ReadLine(); return name; } } При запуске этой программы у пользователя не возникает никаких вопросов: C:\projects\ConsoleFunction> program.exe Вас приветствует моя программа Как вас зовут? chris Здавствуйте, chris! Что хорошо в консольных программах, так это абсолютная синхронность. Спросив о чем#то пользователя, мы сидим и ждем ответа. В графических прило# жениях ту же модель можно было бы реализовать с помощью серии модальных диалогов, но это не вяжется с идеей приложения со средствами навигации. Что# бы перейти к навигационной модели, необходимо ввести асинхронность. Пере# ход к новой странице может произойти через произвольное время, а мы не можем блокировать приложение в ожидании ответа пользователя (напомним, что при# ложение однопоточное, поэтому если блокируется поток графического интер# фейса, то пользователь даже не сможет ничего ввести в текстовое поле!). Начнем с того, что создадим страницу приветствия на базе класса PageFunction. У каждого экземпляра PageFunction есть возвращаемое значение некоторого типа; в данном случае мы ничего не будем возвращать из страницы приветствия, поэто# му точный тип нас не волнует. Пусть это будет Object: public class Welcome : PageFunction<object> { Навигация и страницы 111 public Welcome() { TextBlock text = new TextBlock(); text.FontSize=24; text.Inlines.Add(«Welcome!»); Content = text; } } Далее нужно создать пользовательский интерфейс. Детали программирова# ния интерфейса мы будем рассматривать в главе 3. Рис. 2.23. Первая страничная функция: вывод строки «Welcome!» Класс PageFunction является производным от Page, поэтому всюду, где возмож# на навигация к Page, возможна и навигация к PageFunction. Чтобы вывести стра# ницу приветствия, необходимо создать окно навигации и перейти в него (рис. 2.23): public class Program { [STAThread] static void Main() { Application app = new Application(); NavigationWindow w = new NavigationWindow(); w.Show(); w.Navigate(new Welcome()); app.Run(); } } В консольной программе за выводом приветствия следовало обращение к ме# тоду SayHello. Инкапсуляция логики – важное достоинство функциональной мо# дели программирования, поэтому мы не хотим, чтобы страница приветствия зна# ла о том, как реализован метод SayHello. Далее нужно сделать еще две вещи: соз# дать новый производный от PageFunction класс для реализации SayHello и орга# низовать связь между страницами Welcome и SayHello: public class Welcome : PageFunction<object> { public Welcome() { TextBlock block = new TextBlocks(); Глава 2. Приложения 112 block.FontSize=24; block.Inlines.Add(«Welcome!»); Hyperlink link = new Hyperlink(); link.Inlines.Add(«Next»); link.Click += DoSayHello; block.Inlines.Add(link); Content = block; } void DoSayHello(object sender, RoutedEventArgs e) { NavigationService.Navigate(new SayHello()); } } public class SayHello : PageFunction<object> { public SayHello() { } } Вызов нашего первого метода завершен. До сих пор модель ничем не отличалась от традиционной навигации: в отчет на щелчок мышью происходит переход к следую# щей странице. А вот метод GetName более интересен; у него есть возвращаемое значе# ние. Мы знаем, что SayHello должен получить имя пользователя. Однако на страницу SayHello мы уже перешли. У вызова PageFunction с возвращаемым значением есть два важных аспекта. Во#первых, в силу асинхронной природы навигации завершение функции моделируется событием. Мы подписываемся на событие GetName.Return, чтобы выполнить те или иные действия после завершения функции. Это может пока# заться очевидным, но немного подумав, вы поймете, что страницу SayHello следует выгрузить для того, чтобы отобразилась страница GetName. Для реализации такого поведения можно воспользоваться свойством KeepAlive, которое позволяет удержать SayHello в памяти так, чтобы по завершении GetName был активирован тот же самый экземпляр SayHello. Свойство KeepAlive применимо к любой странице. На рис. 2.24 изображен поток навигации. Сначала создается объект Welcome, а за# тем мы переходим к SayHello. В этот момент объект Welcome уничтожается. Первое, что делает SayHello, – это переход к GetName, но, поскольку KeepAlive равно true, в журнале сохраняется экземпляр SayHello (а не просто URI, которые обычно записы# вается в журнал). После возврата из GetName страницы SayHello отображается снова. навигационный узел навигационный узел содержимое Welcome Переход Журнал навигационный узел содержимое SayHello Журнал навигационный узел содержимое Переход GetName SayHello содержимое Return Журнал Рис. 2.24. Поток навигации со страничными функциями SayHello Журнал Навигация и страницы 113 Для программной реализации описанного потока навигации модифицируем SayHello, так чтобы вызывалась страница GetName. Обращаться к NavigationService.Navigate можно только после инициализации объекта; во время работы конструктора никакого объекта NavigationService еще не существует: public class SayHello : PageFunction<object> { public SayHello() { // Необходимо остаться в памяти, чтобы можно было отреагировать // на возврат из GetName // this.KeepAlive = true; // После инициализации этой страницы мы сначала должны получить // имя пользователя // this.Initialized += DoGetName; } void DoGetName(object sender, EventArgs e) { // Чтобы получить имя пользователя, мы переходим к // GetName и ожидаем ее завершения (события Return) // GetName get = new GetName(); get.Return += GetNameReturned; NavigationService.Navigate(get); } void GetNameReturned(object sender, ReturnEventArgs<string> e) { // Когда GetName вернет управление, мы сможем обновить // содержимое страницы – поприветствовать пользователя // Label label = new Label(); label.Content = string.Format(«Hello {0}», e2.Result); Content = label; } } Осталось еще реализовать страницу GetName. Так как GetName возвращает строку, определим ее как класс, производный от PageFunction<string>. Добавим метку для вы# вода описания, текстовое поле, в которое пользователь сможет ввести имя, и кнопку за# вершения. Если вызывать метод OnReturn, то инфраструктура страничных функций ав# томатически возбудит событие Return и выполнит переход на вызвавшую страницу: public class GetName : PageFunction<string> { public GetName() { StackPanel stack = new StackPanel(); Label label = new Label(); label.Content = «What is your name?»; stack.Children.Add(label); TextBox nameBox = new TextBox(); stack.Children.Add(nameBox); Глава 2. Приложения 114 Button done = new Button(); done.Content = «Done»; done.Click += ReturnName; stack.Children.Add(done); Content = stack; } void ReturnName(object sender, RoutedEventArgs e) { OnReturn(new ReturnEventArgs<string>(nameBox.Text)); } } Отметим, что в реализации GetName вообще не упоминается SayHello. GetName ведет себя как функция в языке программирования, ее можно вызывать откуда угодно, и она всегда будет выполнять возврат на нужную страницу. Страничные функции – это, безусловно, важнейшая инновация, появившаяся в WPF. Раз мы стали думать о вызовах методов как о навигации, а результаты возвра# щать в виде событий, то можно применить к созданию приложений со средствами на# вигации модели, характерные для написания объектно#ориентированного кода. Иног# да простых ссылок на страницы достаточно, но для решения многих задач использо# вание функционально#ориентированной модели навигации намного продуктивнее. Исполнение приложений в браузере Говоря о навигации, люди чаше всего представляют себе приложение, испол# няемое в браузере, например, www.msn.com. WPF#приложения могут исполнять# ся как в автономном окне, так и внутри браузера. XAML#приложения для брау# зера (XBAP – XAML Browser Application) запускаются в окне браузера. Они не устанавливаются на локальную машину навечно и обычно работают в «песочни# це», которой .NET доверяет лишь частично. Не существует универсального ответа на вопрос, когда лучше писать XBAP, в когда стандартное приложение. Во#первых, это зависит от предполагаемого пользо# вателя. С браузерными приложениями работать удобнее (пользователь может прос# то уйти в другое место, а не запускать отдельную программу), и они обычно лучше интегрируются с привычным потоком работ (пользователь сам решает, когда войти в приложение, а когда выйти из него). Традиционные же приложения более модаль# ны (пользователь либо запустил программу, либо нет) и позволяют реализовать бо# лее жесткий контроль (обычно пользователь должен явно завершить программу). Помимо модели работы пользователя, есть еще один вопрос: а может ли вооб# ще приложение нормально функционировать при наличии налагаемых браузе# ром ограничений. Браузерное приложение не может поместить значок в меню Пуск, установить обработчики для расширения имен файлов или оставить еще какой#нибудь постоянный след в компьютере пользователя. Кроме того, обычно оно работает в среде с частичным уровнем доверия, где доступно лишь подмно# жество функциональности .NET Framework19. Например, XBAP#приложению не# 19 Не существует полного перечня функциональности, доступной при частичном уровне доверия. Но в документации по SDK для .NET Framework 3.0 для каждой функции указано, можно ли ее вызы) вать в таких условиях. Исполнение приложений в браузере 115 доступны последние протоколы работы с Web#сервисами, реализованные в Windows Communication Foundation, приходится ограничиваться только подде# ржкой Web#сервисов, имеющейся в .NET 2.0. Можно высказать общую рекомендацию: пользователи предпочитают брау# зерные версии приложений, с которыми работают эпизодически, и автономные версии – для повседневно используемых программ. К счастью, не так уж трудно переключиться с одного варианта на другой, так что можете попробовать реали# зовать обе версии. HelloBrowser У приложения, исполняемого в браузере, должна быть хотя бы одна страница, должно быть задано свойство Application.StartupUri, а в файле проекта необходи# мо прописать свойство HostInBrowser. Одно из ограничений частичного доверия заключается в невозможности создавать и отображать новые окна; все визуаль# ные элементы должны размещаться на страницах: <!— HelloBrowser.csproj —> <Project DefaultTargets=’Build’ xmlns=’http://schemas.microsoft.com/developer/msbuild/2003’> <PropertyGroup> ... <HostInBrowser>True</HostInBrowser> </PropertyGroup> ... </Project> <!— App.xaml —> <Application ... x:Class=’HelloBrowser.MyApp’ StartupUri=’Page1.xaml’ /> <!— page1.xaml —> <Page ... x:Class=’HelloBrowser.Page1’> <TextBlock FontSize=’24pt’>Hello Browser!</TextBlock> </Page> Если бы все было так просто… Увы, для создания приложения, исполняемого в браузере, мы еще должны подписать его манифест20. Надеюсь, что в будущем это ограничение будет снято (не подписываем же мы файлы, содержащие HTML и JavaScript#код!). В Visual Studio создать тестовую подпись сравнительно просто: <Project DefaultTargets=’Build’ xmlns=’http://schemas.microsoft.com/developer/msbuild/2003’> <PropertyGroup> 20 Полная информация о манифестах ClickOnce)приложений включена в WinFX SDK. Глава 2. Приложения 116 ... <HostInBrowser>True</HostInBrowser> <SignManifests>True</SignManifests> <ManifestCertificateThumbprint>X</ManifestCertificateThumbprint> <ManifestKeyFile>HelloBrowser_TemporaryKey.pfx</ManifestKeyFile> </PropertyGroup> ... </Project> Имея подлежащий подписанию манифест, мы можем откомпилировать приложение. В результате будет создан новый файл с расширением XBAP. На рис. 2.25 эти файлы показаны. XBAP#файл представляет собой XML#файл, содержащий информацию о развертывании, точно такую же, как в манифесте развертывания21, используемом для персональных приложений. Наиболее важными являются теги deployment и assemblyIdentity: Компиляция Рис. 2.25. Компиляция XBAP)приложения: исходные и выходные файлы <!— HelloBrowser.xbap —> <?xml version=’1.0’ encoding=’utf 8’?> <asmv1:assembly xsi:schemaLocation=’urn:schemas microsoft com:asm.v1 assembly.adaptive.xsd’ manifestVersion=’1.0’ ...> <assemblyIdentity name=’HelloBrowser.application’ version=’1.0.0.0’ 21 Полная информация о манифестах развертывания в ClickOnce включена в WinFX SDK. Исполнение приложений в браузере 117 publicKeyToken=’7038621db733f8fe’ language=’neutral’ processorArchitecture=’msil’ xmlns=’urn:schemasmicrosoftcom:asm.v1’ /> <description asmv2:publisher=’Microsoft’ asmv2:product=’HelloBrowser’ xmlns=’urn:schemas microsoft com:asm.v1’ /> <deployment install=’false’ /> <dependency> <dependentAssembly dependencyType=’install’ codebase=’HelloBrowser.exe.manifest’ size=’10037’> <assemblyIdentity name=’HelloBrowser.exe’ version=’1.0.0.0’ publicKeyToken=’7038621db733f8fe’ language=’neutral’ processorArchitecture=’msil’ type=’win32’ /> <hash> ... </hash> </dependentAssembly> </dependency> <publisherIdentity name=’CN=XXX’ issuerKeyHash=’XXX’ /> <Signature Id=’StrongNameSignature’ xmlns=’http://www.w3.org/2000/09/xmldsig#’> ... </Signature> </asmv1:assembly> Тег deployment говорит, что приложение не должно устанавливаться; это пре# дотвращает создание значка в меню Пуск и добавление записи в список «Уста# новка и удаление программ» на панели управления. Единственное, что остается в компьютере после того, как пользователь поработает с браузерным приложени# ем, – это записи в истории посещений, куки, посланные Web#сервером, времен# ные файлы Интернета и, возможно, какие#то данные в изолированном хранили# 118 Глава 2. Приложения ще CLR. Все это периодически стирается с помощью тех же средств ОС, которые используются при работе с Web#сайтами. Рис. 2.26. HTML)страница в браузере Internet Explorer Рис. 2.27. Диалог 22 с информацией о загрузке XBAP)приложения в браузере Internet Explorer Тег assemblyIdentity говорит системе, где находится реализация программы, – без этого запустить ее было бы затруднительно! 22 Внимательный читатель обратил внимание на гигантский размер приложения (30 Мб). На самом деле откомпилированные приложения настолько малы, что мне никак не удавалось поймать диа) лог о загрузке на экране. Чтобы сделать этот снимок, я добавил кучу мусорных данных в програм) му, только тогда для загрузки файла потребовалось заметное время. Исполнение приложений в браузере 119 Чаще всего пользователь запускает браузерное приложение, щелкая по ссыл# ке. В данном случае я создал небольшую HTML#страницу со ссылкой, ведущей на XBAP#файл. Поместим ее в тот же каталог, что и сам XBAP#файл: <h1> <a href=’hellobrowser.xbap’>Run Program</a> </h1> При переходе на этот HTML#файл браузер отображает стандартную HTML# страницу (рис. 2.26). Когда пользователь щелкает по ссылке, происходит переход на XBAP#файл, а это приводит к запуску приложения внутри браузера. Если приложение еще не находит# ся в кэше, то можно будет увидеть страницу с информацией о загрузке (рис. 2.27). Рис. 2.28. XBAP)приложение в браузере Internet Explorer При запуске приложения вы увидите картинку, изображенную на рис. 2.28. Наша цель – обеспечить удобную работу пользователя без создания дополни# тельных окон, диалогов и других отвлекающих моментов. Под капотом WPF запускает приложения за счет реализации ActiveX#объекта DocObject (или OLE#объекта, терминология зависит от того, когда вы изучали предмет). Любое приложение, поддерживающее такие объекты, может исполнять WPF# приложения. При установке .NET Framework 3.0 регистрируется обработчик 120 Глава 2. Приложения MIME#типа для расширения XBAP, который загружает WPF. Все исполняемые в браузере приложения WPF исполняет вне процесса#владельца, используя для этой цели ведущее приложение PresentationHost. На самом верхнем уровне задача отображения ложится на два приложения: Internet Explorer и PresentationHost23. Весь пользовательский интерфейс Internet Explorer (адресную строку, панель инструментов и т.д.) отображает процесс iex# plore.exe. Управляет этими элементами IE, а настроить их можно только с по# мощью существующих COM#интерфейсов (IHtmlWindow и т.д.). Рис. 2.29. Исполнение в браузере: взгляд со стороны PresentationHost На рис. 2.29 показан пользовательский интерфейс, который отображает WPF. При работе внутри браузера создается специальное окно верхнего уровня, назы# ваемое корневым окном браузера. Его задача – быть владельцем страниц, между которыми осуществляется навигация. Напомним, что, говоря о создании прило# жения, исполняемого в браузере, мы подчеркнули необходимость задания свой# ства Application.StartupUri, которое должно указывать на страницу. Связано это с тем, что механизм навигации автоматически создает это специальное окно и в нем осуществляет переход на указанную страницу. 23 Есть тонкая, но важная деталь, касающаяся безопасности работы PresentationHost. В Windows XP исполняемый файл presentation.exe запускается с модифицированными разрешениями NT; по су) ществу, мы отщепляем административный маркер от всех разрешений, с которыми работает про) цесс iexplore.exe. В Windows Vista механизм доступа со стороны пользователя с ограниченными правами обеспечивает ту же модель безопасности глобально ) для всех работающих в системе приложений. Исполнение приложений в браузере 121 Если приложение исполняется в браузерах старых версий, то иногда появляется вторая строка с кнопками навигации. Если браузер не поддерживает новые API нави# гации, то в корневом окне автоматически отображается навигационный интерфейс. При просмотре в браузере любого содержимого, отображаемого WPF, исполь# зуется один и тот же базовый механизм. Так можно просматривать содержимое разных видов: XBAP#приложения, документы в формате XML Paper Specification (XPS) и простые текстовые XAML#файлы. Рис. 2.30. Просмотр XAML)файл в браузере Независимая разметка Помимо компиляции приложения в XBAP#файл, который может исполняться в браузере, мы можем просматривать и обычные XAML#файлы. При этом разре# шается использовать те же элементы, что и в XBAP#приложении, а наличие фай# ла с кодом не допускается. При публикации небольших документов или для прос# мотра простого содержимого с поддержкой навигации эта техника позволяет обойтись вовсе без компиляции. Поскольку независимая XAML#разметка не поддерживает наличия файла с кодом, то нельзя употреблять тег x:Class, который присутствовал в странице для XBAP#приложения. Зато можно взять ту же самую страницу, сохранить ее как файл с расширением XAML, дважды щелкнуть по нему мышью, и вы увидите страницу в браузере (рис. 2.30). <Page xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’> <TextBlock FontSize=’24pt’>Hello Loose XAML!</TextBlock> </Page> 122 Глава 2. Приложения Чего мы достигли? В этой главе мы рассмотрели центральный объект приложения – Application, а также основные компоненты, из которых приложение состоит: окна, страницы и пользовательские элементы управления. С помощью объектов Window мы можем реализовать различные оконные модели, а страницы и механизм навигации поз# воляют легко создавать приложения, поддерживающие традиционную или функ# циональную навигацию. Службы, предоставляемые моделью приложений в WPF, упрощают управление состоянием для приложений любого вида. В WPF предпринята попытка построить облегченную модель, достаточно гибкую для создания разнообразных приложений и в то же время предоставляющую обшир# ный арсенал служб, помогающих их разрабатывать. 123 Глава 3. Элементы управления В главе 2 мы описали порядок создания приложения, основные компоненты пользовательского интерфейса, службы приложений и варианты их исполнения. Хотя интерфейс любого приложения состоит из окон, пользовательских элемен# тов и страниц, части, расположенные на верхнем уровне, строятся из более мел# ких компонентов – элементов управления. Сколько существуют библиотеки для построения графических интерфейсов, столько же существуют и элементы управления. Называются ли они виджеты, гад& жеты, VBX, OCX, компоненты, CWnd, элементы или как#то по#другому, всегда при# сутствовала идея компонента, инкапсулирующего некое поведение, объектную мо# дель и отображение. Windows Presentation Foundation не является исключением. В этой главе мы приступим к подробному изучению элементов управления, которые составляют кровь и плоть WPF. Для начала рассмотрим новые концеп# ции, появившиеся в WPF: модель содержимого и шаблоны. В совокупности они составляют основу всей модели элементов управления; усвоив их, вы заложите фундамент для понимания библиотеки таких элементов в WPF. Кроме того, мы более углубленно займемся некоторыми строительными блоками, с помощью ко# торых можно создавать свои собственные элементы. Принципиальные основы элементов управления Команда, собранная для работы над библиотекой элементов управления WPF, раньше уже занималась разработкой многих каркасов. У нас был составлен перечень допущенных в прошлом ошибок, которые мы не хотели повторять. Кро# ме того, мы понимали, что WPF должна стать интегрированной платформой для пользовательского интерфейса, документов и мультимедиа. В предшествующих каркасах одной из ключевых проблем было отсутствие последовательности и гибкости. Для создания нестандартного списка в Win32 приходилось пользоваться моделью программирования, которая в корне отлича# лась от модели использования элемента. В современных версиях Windows кноп# ки в разных местах реализованы совершенно по#разному. Так, кнопки в полосе прокрутки ведут себя иначе, чем кнопка закрытия окна, а та не похожа на станда# ртную кнопку в диалоговом окне. Команда, разрабатывавшая WPF, хотела соз# дать такую систему, в которой одна и та же кнопка могла бы использоваться пов# семестно. Поэтому в основу WPF был положен принцип композиции элементов. Чтобы интегрировать пользовательский интерфейс, документы и мультиме# диа, нам нужен был какой#то принцип проектирования, охватывающий все слу# чаи употребления развитого содержимого. В ранних версиях HTML допуска# Глава 3. Элементы управления 124 лось включать обогащенный текст с разными шрифтами, кеглями, цветами и плотностью практически в любое место, за исключением кнопки. Даже для того чтобы просто изменить цвет кнопки, приходилось создавать графическое изобра# жение. Win32 ушла немногим дальше; единственным элементом, в котором мог присутствовать обогащенный текст, был RichEdit. Принцип развитого содержи# мого говорит, что в любом месте, где допускается появление текста, должны под# держиваться обогащенный текст, мультимедиа и другие элементы управления. Весь каркас .NET пронизан одной идеей – упростить модель программирова# ния. Этот же принцип мы применили и к элементам управления в WPF. Простая модель программирования – критически важная характеристики набора элемен# тов управления, которым разработчики могли бы пользоваться на практике. Уже на ранних стадиях работы над WPF возник простой (как нам казалось) вопрос: какой должна быть объектная модель кнопки? Нажимаемая кнопка – это один из самых распространенных элементов управления; она существует в Windows, начиная с версии 1.0, а в Macintosh – начиная с Mac OS 1.0. Без нее просто никуда! Но тут команда WPF налетела на сук. Композиция элементов Развитое содержимое Простая модель программирования Рис. 3.1. Три принципа элементов управления: композиция, развитое содержимое и простая модель программирования На рис. 3.1 показана кнопка, содержащая две других кнопки, кнопка с разви# тым содержимым и простая модель программирования, к которой мы стреми# лись. Первоначально мы рассмотрели композицию элементов и развитое содер# жимое и решили, что коль скоро развитое содержимое на деле состоит из элемен# тов, то композиция элементов – это все, что нам необходимо. Разработанная мо# дель программирования выглядела примерно так: Button b = new Button(); Ellipse left = new Ellipse(); TextBlock text = new TextBlock(); text.Text = «Hello World»; Ellipse right = new Ellipse(); b.Children.Add(left); b.Children.Add(text); b.Children.Add(right); Неплохо, но мы упустили одну вещь. Что, если слово World во фразе «Hello World» должно быть выведено полужирным шрифтом? Button b = new Button(); Принципиальные основы элементов управления 125 Ellipse left = new Ellipse(); TextBlock text = new TextBlock(); text.Inlines.Add(new Run(«Hello «)); text.Inlines.Add(new Bold(«World»)); Ellipse right = new Ellipse(); b.Children.Add(left); b.Children.Add(text); b.Children.Add(right); Но даже если нужно что#то совсем простое, например кнопка с надписью «OK», то код получится примерно таким: Button b = new Button(); Text text = new Text(); text.Text = «OK»; b.Children.Add(text); Сейчас вы должны задаться вопросом: что сталось с принципом простой мо# дели программирования? Ведь в действительности хотелось бы, чтобы этот код выглядел не сложнее чем: Button b = new Button(); b.Text = «OK»; Рис. 3.2. Надпись «Hello World» на кнопке Здесь для добавления содержимого в кнопку хватило одного свойства, однако содержимое может быть только строкой. А нам бы хотелось поддержать значи# тельно более сложное содержимое. Вот мы и подошли к модели содержимого. Модель содержимого При программировании на уровне Win32 элементы управления (например, ListBox, Button и Label) традиционно в качестве данных содержат строки. При проектировании WPF хотелось, с одной стороны, поддержать повсеместное при# сутствие развитого содержимого, а, с другой, отделить данные от визуализации. Во многих системах применяются сложные способы разделения модели, вида и контроллера. При этом требуется, чтобы программист понимал модель данных каждого элемента управления. Вместо этого в WPF используется модель, уже знакомая многим разработчикам: система типов CLR. Начнем с задания содержимого кнопки: Button b = new Button(); b.Content = «Hello World»; Глава 3. Элементы управления 126 Здесь, как и следовало ожидать, создается простая кнопка с надписью «Hello World» (рис. 3.2). Отметим, что тип свойства Content объекта Button – это System.Object, а не строка. Рис. 3.3. Кнопка и составляющие ее элементы Чтобы разобраться в происходящем, рассмотрим созданное изображение. Мы знаем, что его корнем является кнопка, но где#то должно находиться нечто, отоб# ражаемое как текст. Поскольку мы сторонники композиции элементов, то для ри# сования кнопки воспользовались другими элементами. На рис. 3.3 показана ие# рархия элементов, которая называется также деревом отображения. Среди составных элементов отображения на рис. 3.3 мы видим элемент ButtonChrome, который отвечает за вывод фона кнопки и еще два интересных элемента: ContentPresenter и TextBlock. С элементом TextBlock мы уже встреча# лись выше, этот класс выводит простой текст. Но что такое ContentPresenter? Элемент ContentPresenter ContentPresenter – это рабочая лошадка модели содержимого. Он принимает любые данные, помещенные в свойство Content и создает соответствующее им дерево визуального отображения. Например, ContentPresenter можно использо# вать для вывода числа, даты и времени или кнопки: public Window1() { StackPanel panel = new StackPanel(); ContentPresenter intPresenter = new ContentPresenter(); intPresenter.Content = 5; panel.Children.Add(intPresenter); ContentPresenter datePresenter = new ContentPresenter(); datePresenter.Content = DateTime.Now; panel.Children.Add(datePresenter); ContentPresenter elementPresenter = new ContentPresenter(); elementPresenter.Content = new Button(); panel.Children.Add(elementPresenter); Content = panel; } Эта программа выводит данные разных типов, как показано на рис. 3.4. Принципиальные основы элементов управления 127 Возникает вопрос; «Как это делается?» Некоторые подсказки дает объектная модель ContentPresenter: public class ContentPresenter : FrameworkElement { ... public object Content { get; set; } public string ContentSource { get; set; } public DataTemplate ContentTemplate { get; set; } public DataTemplateSelector ContentTemplateSelector { get; set; } ... } Рис. 3.4. Использование нескольких объектов ContentPresenter для вывода различных данных и соответствующее дерево отображения Первое, на что хочется обратить внимание при знакомстве с объектом Content Presenter, – это тип данных содержимого. Если содержимое уже принадлежит типу System.Windows.UIElement (базовый класс всех элементов управления), то больше ничего делать не нужно – содержимое может быть сразу же добавлено в дерево отоб# ражения. В противном случае ContentPresenter пытается найти альтернативные спо# собы преобразовать содержимое в визуальный образ, пробуя следующие варианты: 1. Если Content принадлежит типу UIElement, добавить в дерево отображения. 2. Если задано свойство ContentTemplate, использовать шаблон для создания экземпляра класса UIElement, а затем добавить этот экземпляр в дерево отображения. 3. Если задано свойство ContentTemplateSelector, использовать его для поис# ка шаблона, затем по шаблону создать экземпляр класса UIElement и доба# вить его в дерево отображения. 4. Если с типом данных Content ассоциирован шаблон данных1, использовать его для создания экземпляра класса UIElement. 1 Шаблоны данных рассматриваются в главе 6. Глава 3. Элементы управления 128 5. Если с типом данных Content ассоциирован объект класса TypeConverter, который способен преобразовать содержимое к типу UIElement, выпол# нить такое преобразование и добавить результат в дерево отображения. 6. Если с типом данных Content ассоциирован объект TypeConverter, кото# рый способен преобразовать содержимое в строку, обернуть содержимое в объект TextBlock и добавить его в дерево отображения. 7. Если больше ничего не остается, вызвать метод ToString объекта Content, обернуть результат в объект TextBlock и добавить его в дерево отображе# ния. Таблица 3.1. Схема именования свойства, в котором хранится содержимое Количество Один Несколько Объект(ы) Элемент(ы) Content Child Items Children Как видим, ContentPresenter прилагает все усилия к тому, чтобы создать неч# то, допускающее отображение. Единственное значение, которое гарантированно не будет отображено, – это null; все остальное так или иначе отображается, при# чем в худшем случае будет выведен результат, возвращаемый методом ToString объекта#содержимого. Инкапсулировав логику представления содержимого в простой, допускающий композицию элемент, мы получили возможность повтор# ного использования. Пока что мы видели, как элемент управления Button поль# зуется объектом ContentPresenter, чтобы реализовать простую модель програм# мирования, не жертвуя развитым содержимым и композицией элементов. Суще# ствует четыре разновидности модели содержимого, различающиеся тем, что де# лает элемент управления (например, может ли он принимать на входе любые дан# ные или только элементы?), и является ли модель одиночной или множествен# ной. Каждая разновидность инкапсулирована в виде свойства, как показано в табл. 3.1. Мы уже видели, как для отображения содержимого применяется свойство Content, теперь познакомимся со свойствами Items, Child и Children. Свойство Items Если класс ContentPresenter так хорош для одиночного элемента, то почему бы не взять его за основу для списка элементов. Вариант модели множественно# го содержимого очень похож на случай одиночного содержимого, только вместо свойства Content, значением которого является объект, мы применяем свойство Items, для которого значением служит список: ListBox l = new ListBox(); l.Items.Add(«Hello»); Принципиальные основы элементов управления 129 l.Items.Add(«There»); l.Items.Add(«World»); При взгляде на результирующее дерево отображения (рис. 3.5) хочется воск# ликнуть «Однако!». Да, оно получилось не маленьким. В дереве отображения для ListBox есть нес# колько объектов ContentPresenter. Начав снизу (элементы, напечатанные более крупным шрифтом), мы наблюдаем знакомый по Button паттерн: объект ContentPresenter, содержащий элемент TextBlock. ListBoxItem соответствует мо# дели одиночного содержимого – один элемент управления со свойством Content, как и в случае Button. Рис. 3.5. Элемент ListBox, содержащий несколько строк, и его дерево отображения Поднимаясь выше, мы доходим до объекта ItemsPresenter. Если ContentPresenter – основа для отображения элементов, содержимое которых сос# тоит из одного объекта, то ItemsPresenter предназначен для вывода элементов с множественным содержимым. Этот класс, работая совместно с несколькими дру# гими, динамически генерирует необходимые объекты, каждый из которых поль# зуется экземпляром класса ContentPresenter. Последний объект, в название которого есть слово «presenter», – это ScrollContentPresenter, который обеспечивает прокрутку списка. Этот объект явля# ется не частью базовой модели содержимого, а просто реализацией примитивного элемента управления ScrollViewer, отвечающего за прокрутку любого содержимого. Свойства Children и Child Некоторые элементы управления поддерживают в качестве содержимого не произвольные объекты (как, скажем, Button и ListBox), а только потомков клас# са UIElement. И это подводит нас к оставшимся двум разновидностям содержи# мого: Child и Children. 130 Глава 3. Элементы управления Прежде чем переходить к моделям содержимого элементов, будет интересно немного поговорить о типах элементов управления. Часто приводят высказыва# ние Джеффа Богдана (Jeff Bogdan), еще одного архитектора WPF: «Приступая к факторизации, самое главное – знать, когда остановиться». Композиция элемен# тов – вещь замечательная, но в конце концов кто#то же должен выполнить содер# жательную работу. В WPF большая часть элементов управления относится к од# ной из трех категорий: составной элемент, менеджер размещения и элемент#ри# совальщик. Составной элемент (content control), например ListBox и Button, сам ничего не делает; он составлен из других элементов, которые и выполняют реальную ра# боту. Например, в дереве отображения кнопки Button (рис. 3.3) мы видим, что визуализацией кнопки занимаются элементы ButtonChrome и Content#Presenter. С другой стороны, менеджеры размещения (layout control) отвечают за пози# ционирование других элементов управления. Выше мы уже встречались с эле# ментом такого вида: StackPanel. Вообще говоря, сами менеджеры размещения не# видимы, мы наблюдаем лишь их воздействие на другие элементы. Большинство элементов, поддерживающих модель множественного содержимого, – это как раз менеджеры размещения. Все панели, кроме FlowDocumentViewer2, реализуют модель множественного содержимого, наследуя классу Panel: public class Panel : FrameworkElement { public UIElementCollection Children { get; } } Рисовальщики (render controls) – это элементы, которые рисуют пиксели на экране. В качестве примеров можно привести элементы Rectangle и Ellipse. А самым лучшим примером, пожалуй, будет класс Border. Он рисует рамку вокруг переданного ему единственного элемента: public class Border : Decorator { public UIElement Child { get; set; } } Модель содержимого помогает решить проблему вывода развитого содержимого при сохранении простой модели программирования. Еще один сложный вопрос, воз# никающий при написании элементов управления, – как нарисовать то, что не явля# ется содержимым? Например, как внутри кнопки появляется объект ButtonChrome? Шаблоны Изменение внешнего вида элемента управления может оказаться нетривиальной задачей. В Windows Forms или User32 для изменения вида кнопки нужно переопре# делить метод OnPaint или обработать сообщение WM_PAINT и самостоятельно на# писать код, который будет рисовать пиксели. В HTML требуется создать из отдель# ных изображений элемент управления, который будет выглядеть как кнопка, или 2 Похоже, без исключений никак не обойтись. Принципиальные основы элементов управления 131 написать специальную разметку. Проблема во всех этих случаях заключается в том, что модель программирования, применяемая при создании и изменении внешнего вида элемента управления, существенно отличается от модели его использования. Вместо того чтобы заново писать весь код отображения, часто применяют дру# гое решение: настройку с помощью свойств. В HTML для изменения внешнего вида элемента можно воспользоваться каскадными таблицами стилей. Проблема однако в том, что от автора элемента требуется представить все возможные наст# ройки в виде свойств, к которым можно применить CSS#стили. В современных реализациях CSS имеется обширная библиотека свойств для изменения границ, цветов и форматирования текста, но модифицировать отображение путем встав# ки произвольного содержимого нельзя. WPF позволяет изменять способ визуализации элементов управления в ши# роких пределах с помощью свойств, но, кроме того, есть возможность полностью модифицировать внешний вид любого элемента. Мы хотели, чтобы весь спектр средств настройки был декларативным, но при этом сохранялась последователь# ная модель программирования. В результате остановились на системе шаблонов. В дереве отображения кнопки Button (рис. 3.6) присутствует элемент ButtonChrome, не являющийся ни кнопкой, ни объектом ContentPresenter, ни со# держимым. Он был создан шаблоном кнопки. Выше при обсуждении модели содержимого мы ввели в рассмотрение три ти# па элементов управления: рисовальщики, менеджеры размещения и составные элементы. Все составные элементы не только реализуют представление содержи# мого, но и поддерживают шаблоны. Шаблон позволяет создать конкретный на# бор элементов (например, ButtonChrome) для реализации отображения. Состав# ные элементы наследуют классу Control и получают от него свойство Template. Рис. 3.6. Дерево отображения элемента Button (обратите внимание на ButtonChrome) Рис. 3.7. Кнопка с шаблоном)прямоугольником и ее дерево отображения 132 Глава 3. Элементы управления Для начала рассмотрим код простой кнопки, с которой мы уже неоднократно встречались: <Button> My Button </Button> Чтобы изменить ее внешний вид, не затрагивая содержимого, можно было оп# ределить новый шаблон. Шаблон представляет собой фабрику для создания но# вого дерева отображения. Есть две разновидности шаблонов: ControlTemplate (он создает дерево отображения для элемента управления) и DataTemplate (соз# дает дерево отображения для данных, мы будем говорить о таких шаблонах в гла# ве 6). Чтобы определить новый шаблон для элемента управления, нужно задать целевой тип и дерево отображения. Целевой тип сообщает шаблону о том, к объ# ектам какого типа он будет применяться: <Button> <Button.Template> <ControlTemplate TargetType=’{x:Type Button}’> <Rectangle Fill=’Red’ Width=’75’ Height=’23’ /> </ControlTemplate> </Button.Template> My Button </Button> В этом примере дерево отображения представляет собой один красный прямо# угольник. На рис. 3.7 показано, как выглядят сгенерированный элемент и его де# рево отображения. Стоит отметить отсутствие объектов ButtonChrome, ContentPresenter и TextBlock, которые мы видели раньше. И тем не менее кнопка вполне работоспо# собна. Чтобы убедиться в этом, напишем код, который будет изменять шаблон при нажатии кнопки. Прежде всего, определим обработчик события, в котором новый шаблон создается и ассоциируется с кнопкой. void ChangeIt(object sender, RoutedEventArgs e) { ControlTemplate template = new ControlTemplate(typeof(Button)); // Это пустой шаблон, который ничего не делает ((Button)sender).Template = template; } Имея шаблон, нужно заполнить дерево отображения. Оно задается с по# мощью свойства VisualTree объекта ControlTemplate, которое имеет тип Framework ElementFactory. Шаблон можно применять к нескольким элементам управления, но каждый элемент должен встречаться в дереве не более одного раза. Чтобы решить эту проблему, объект FrameworkElementFactory конструи# рует новый экземпляр дерева отображения для каждого элемента, использую# щего данный шаблон. Ниже мы говорим, что кнопка должна отображаться в ви# де эллипса: Принципиальные основы элементов управления 133 void ChangeIt(object sender, RoutedEventArgs e) { ControlTemplate template = new ControlTemplate(typeof(Button)); // Шаблон «создать невидимый эллипс» template.VisualTree = new FrameworkElementFactory(typeof(Ellipse)); ((Button)sender).Template = template; } Далее мы хотим задать некоторые свойства эллипса. Для установки свойств вызывается метод SetValue, так как мы по сути дела создаем список свойств, которым нужно будет присвоить значения при создании эллипса: void ChangeIt(object sender, RoutedEventArgs e) { ControlTemplate template = new ControlTemplate(typeof(Button)); // Шаблон «создать синий эллипс» template.VisualTree = new FrameworkElementFactory(typeof(Ellipse)); template.VisualTree.SetValue(Ellipse.FillProperty, Brushes.Blue); template.VisualTree.SetValue(Ellipse.WidthProperty, 75); template.VisualTree.SetValue(Ellipse.HeightProperty, 23); ((Button)sender).Template = template; } Возможно, вы обратили внимание, что, не привязав разметку к объектной мо# дели, мы нарушили один из принципов языка XAML. В данном случае код, экви# валентный определению шаблона, очень сильно отличается от разметки. Поэто# му мы решили отступить от догмы, но сделать разметку более удобной. Эта раз# метка заново интерпретируется при каждом использовании шаблона. Последний шаг – связать обработчик события с кнопкой: <Button ... Click=’ChangeIt’> ... </Button> На рис. 3.8 показан результат нажатия кнопки. Рис. 3.8. Кнопка и ее дерево отображения после применения нового шаблона Добавив немного эстетических деталей и включив объект ContentPresenter, мы можем сделать так, чтобы кнопка была похожа на кнопку: <ControlTemplate TargetType=’{x:Type Button}’> <Border CornerRadius=’4’ BorderThickness=’3’> <Border.BorderBrush> Глава 3. Элементы управления 134 <LinearGradientBrush EndPoint=’0,1’> <LinearGradientBrush.GradientStops> <GradientStop Offset=’0’ Color=’#FFFFFFFF’ <GradientStop Offset=’1’ Color=’#FF777777’ </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Border.BorderBrush> <Border.Background> <LinearGradientBrush EndPoint=’0,1’> <LinearGradientBrush.GradientStops> <GradientStop Offset=’0’ Color=’#FF777777’ <GradientStop Offset=’1’ Color=’#FFFFFFFF’ </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Border.Background> <ContentPresenter HorizontalAlignment=’Center’ VerticalAlignment=’Center’ /> </Border> </ControlTemplate> /> /> /> /> Включенный в шаблон объект ContentPresenter по умолчанию ищет свойство Content у элемента, к которому применен шаблон, и отображает его значение. Чтобы визуализировать рамку, мы заменили стандартный для кнопки элемент ButtonChrome обобщенным элементом Border. Результат работы этой програм# мы куда больше напоминает кнопку (рис. 3.9). А что произойдет, если в этом шаблоне изменить цвет фона кнопки? Рис. 3.9. Кнопка с более эстетичным шаблоном и ее дерево отображения Привязка шаблона Вспомните о трех принципах элементов управления: композиция, возмож# ность вставлять развитое содержимое в любое место и простая модель програм# мирования. Шаблоны отвечают первым двум условиям, однако требование опре# делять шаблон только для того, чтобы изменить цвет кнопки, вряд ли можно наз# вать вершиной простоты. Принципиальные основы элементов управления 135 В идеале хотелось бы иметь возможность добавлять к шаблону параметры или вы# полнять дополнительную настройку за счет свойств шаблонного элемента. Тогда автор шаблона мог бы выполнить привязку к свойствам шаблонного элемента, что позволи# ло бы пользователю менять свойства элемента управления и настраивать шаблон: <ControlTemplate TargetType=’{x:Type Button}’> <Border CornerRadius=’4’ BorderThickness=’{TemplateBinding Property=BorderThickness}’ BorderBrush=’{TemplateBinding Property=BorderBrush}’ Background=’{TemplateBinding Property=Background}’ > <ContentPresenter /> </Border> </ControlTemplate> В этой разметке свойства BorderThickness, BorderBrush и Background рамки Border привязываются к одноименным свойствам элемента Button, с которым ас# социирован шаблон. Теперь для создания кнопки, показанной на рис. 3.10, доста# точно просто задать свойства элемента Button: <Button BorderThickness = ‘4’ BorderBrush = ‘Orange’ Background = ‘Yellow’ > <Button.Template>...</Button.Template> </Button> Рис. 3.10. Кнопка с шаблоном, в котором используется привязка В данном случае имена свойств совпадают, но это не обязательно. Вот как можно было бы выполнить привязку для исходной прямоугольной кнопки: <ControlTemplate TargetType=’{x:Type Button}’> <Rectangle Fill=’{TemplateBinding Property=Background}’ /> </ControlTemplate> Глава 3. Элементы управления 136 В обоих примерах достигается одна и та же цель: дать разработчику возмож# ность изменять внешний вид элемента управления с помощью одних лишь свойств. Другой важной частью идеологии WPF является инкрементная настрой# ка. Мы начинаем настройку внешнего вида приложения с базовых элементов и их свойств (к примеру, с задания свойства Background для элемента Button). Затем мы переходим к составлению одних элементов из других (помещаем изображение внутрь кнопки). Далее мы можем создать шаблон для элемента управления, а, ес# ли без этого не обойтись, то даже написать специализированный элемент. Размышления о шаблонах В WPF шаблоны используются повсеместно. Мы ставили себе целью сделать настраиваемым все стороны пользовательского интерфейса, создав тем самым совершенно новые модели взаимодействия. Быть может, для вас это будет нео# жиданностью, но Window – это тоже элемент управления, поддерживающий мо# дель содержимого. Мы можем написать шаблон для Window: <ControlTemplate TargetType=’{x:Type Window}’> <Grid> <!— фон —> <Rectangle> ... </Rectangle> <!— полоса заголовка —> <Rectangle ...> ... </Rectangle> <TextBlock ... Text=’{TemplateBinding Property=Title}’ /> <!— содержимое окна —> <ContentControl Margin=’40,70,40,40’ /> ... </Grid> </ControlTemplate> Рис. 3.11. Шаблон окна позволяет определить общий стиль для всего пользовательского интерфейса Библиотека элементов управления 137 В этом шаблоне использовано много визуальных элементов и несколько эле# ментов управления. В результате вместо окна со стандартным белым фоном мы получили нечто куда более привлекательное (рис. 3.11), и для этого не пришлось ни писать новый базовый класс, ни изменять содержимое Window. Эта техника позволяет задавать визуальные характеристики буквально для каждого аспекта приложения! Библиотека элементов управления Вооружившись двумя важнейшими концепциями – моделью содержимого и шаблонами, мы можем начать обзор библиотеки элементов управления, входя# щей в состав WPF. В ней имеются почти все стандартные элементы, которые принято включать в любой пакет для создания графических интерфейсов (за немногими исключениями, например, DataGrid). Поскольку модель содержимо# го и шаблоны позволяют изменять способ визуализации в очень широких преде# лах, то определяющим признаком элемента управления становится его модель данных и модель взаимодействия. Click Button ToggleButton ButtonBase CheckBox Визуальные элементы и взаимодействие RadioButton IsChecked, IsThreeState Рис. 3.12. Иерархия класса Button Кнопки Наше путешествие естественно начать с элемента Button. Важнейшей особен# ностью кнопки является ее «нажимаемость»3. К событию Click, унаследованному от ButtonBase, класс Button не добавляет разве что внешний вид стандартной кнопки. Классы CheckBox и RadioButton представляют кнопки с переключением, име# ющие свойства IsChecked (модель данных) и IsThreeState (модель взаимодей# 3 В какой)то момент мы подумывали назвать базовый класс не ButtonBase, а System.Windows.Controls.Clickable, но потом отказались от этой идеи. 138 Глава 3. Элементы управления ствия). Если IsThreeState равно True, то, щелкая по флажку, пользователь может переводить его в одно из трех состояний: Checked, Unchecked или Indeterminate. Если же IsThreeState равно False, то пользователь никакими манипуляциями не сможет получить состояние Indeterminate, хотя его можно установить программно. На рис. 3.12 показана иерархия класса Button. Классы ButtonBase и ToggleButton служебные, напрямую они, как правило, не используются. Однако классы Button, CheckBox и RadioButton расширяют их, реализуя конкретную стратегию взаимодействия с пользователем (так, RadioButton гарантирует, что отмечена будет только одна из набора кнопок) и визуальное представление (RadioButton рисуется в виде привычного набора переключателей). Рис. 3.13. Несколько элементов управления, производных от кнопки В следующем примере демонстрируется стандартный внешний вид всех трех кнопочных элементов управления: <StackPanel Orientation=’Horizontal’> <Button Margin=’5’ VerticalAlignment=’Top’>Click</Button> <StackPanel Margin=’5’ VerticalAlignment=’Top’> <CheckBox IsChecked=’True’>Click</CheckBox> <CheckBox IsChecked=’False’>Click</CheckBox> <CheckBox IsChecked=’{x:Null}’>Click</CheckBox> </StackPanel> <StackPanel Margin=’5’ VerticalAlignment=’Top’> <RadioButton>Click</RadioButton> <RadioButton IsChecked=’True’>Click</RadioButton> <RadioButton>Click</RadioButton> </StackPanel> </StackPanel> Как видно из рис. 3.13, эти элементы похожи на встроенные в Windows4, а, за# пустив программу, вы убедитесь, что логика взаимодействия ничем не отличает# ся. Мы хотели, чтобы основные элементы в WPF были идентичны встроенным с точностью до пикселя5. 4 5 Интересно отметить, что в WPF для обозначения клавиши)акселератора применяется символ "_", а не "&". Поэтому, чтобы создать кнопку с надписью "OK" и сделать клавишу "O" акселератором, надо было бы написать <Button>_OK</Button>. Это изменение было внесено, главным образом, из)за того, что правила XML требуют вместо символа "&" всюду писать "&amp;", а это слишком длинно. Насколько это возможно, конечно. Библиотека элементов управления 139 Списки Одна из самых распространенных задач в любом приложении – отображе# ние списка данных. Списковые элементы управления в WPF обеспечивают двоякую функциональность: отображение списка данных и возможность выбо# ра из него одного или нескольких элементов. В ходе разработки WPF мы мно# го спорили, сколько включить списковых элементов управления. Из#за нали# чия в WPF шаблонов такие элементы, как ListBox, ComboBox, DomainUpDown и даже нечто похожее на список переключателей различают# ся лишь шаблонами, надстроенными над одним базовым элементом. В конце концов было решено поставлять все привычные элементы управления, но от# давать предпочтение не новым элементам, а шаблонам (именно поэтому был исключен элемент RadioButtonList, который присутствовал в бета#версии). В WPF есть четыре стандартных списковых элемента: ListBox, ComboBox, ListView и TreeView. Данные для любого спискового элемента могут поступать из двух источников: свойство ItemsSource или свойство Items. Свойство Items помещает данные во внутренний список: ListBox list = new ListBox(); list.Items.Add(«a»); list.Items.Add(«b»); С помощью свойства ItemsSource элементу передается набор данных, который отображается в списке: string[] items = new string[] { «a», «b» }; ListBox list = new ListBox(); list.ItemsSource = items; Между этими способами есть тонкое, но важное различие: свойство ItemsSource позволяет хранить данные отдельно от элемента управления. Элементы ListBox и ComboBox На рис. 3.14 изображены два основных вида списков: ListBox и ComboBox. Конечно, ComboBox обеспечивает иное, нежели ListBox, представление, но с точки зрения объектной модели эти два элемента почти тождественны. Пер# воначально выпадающая часть ComboBox была реализована с помощью ListBox. Лучше пользоваться свойством ItemsSource. Хотя в качестве источника данных для списка может выступать любой тип, реализующий интерфейс IEnumerable, в .NET 3.0 включен новый тип набора, предназначенные специ# ально для таких случаев: ObservableCollection<T>. Этот тип реализует нес# колько интерфейсов для отслеживания изменений, которые делают его более удобным в качестве источника данных для списков (детали рассматриваются в главе 6). Глава 3. Элементы управления 140 Рис. 3.14. Два основных вида списков: ListBox и ComboBox Как вы, наверное, догадались, памятуя о двух фундаментальных концепциях элементов управления – модели содержимого и шаблонах, мы можем поместить в списковые элементы любые данные и изменять их внешний вид с помощью шаблонов. Кроме того, у элементов ListBox и ComboBox есть ряд свойств для из# менения облика без написания нового шаблона. Наверное, чаще всего для настройки списка применяется свойство ItemsPanel. Оно позволяет определить шаблон для создания панели размещения, которая служит для отображения элементов списка. Подробнее о размещении мы будем говорить в главе 4. Чтобы понять, как пользоваться этим свойством, вспомните панель управле# ния в Windows XP (рис. 3.15). Это объект типа ListBox (поскольку возможность выбора не обязательна, можно было бы взять и базовый класс ItemsControl), эле# менты которого размещены в ячейках сетки. Объекту ItemsPanel передается шаб# лон ItemsPanelTemplate (не обычный объект ControlTemplate), который требует, чтобы шаблон построил панель. В качестве менеджера размещения для списка можно было использовать класс UniformGrid, который дает результат, очень близкий к панели управления (рис. 3.16): <ListBox ...> <ListBox.ItemsPanel> <ItemsPanelTemplate> <UniformGrid Columns=’2’/> </ItemsPanelTemplate> </ListBox.ItemsPanel> </ListBox> Но существенно дальше с помощью одного лишь свойства ItemsPanel мы не продвинемся; рано или поздно возникнет желание создать для списка специаль# ный шаблон. Шаблон для списка строится точно так же, как для любого другого элемента управления, но с одним важным исключением: списковому элементу нужно место, в котором будут отображаться его данные. Когда для настройки Библиотека элементов управления 141 списка применяется свойство ItemsPanel, элемент знает, куда помещать данные. Если же мы пользуемся свойством Template, то списковый элемент должен поис# кать в своем дереве отображения панель, на которую следует выводить данные. Нужную панель (одну или несколько) мы помечаем свойством IsItemsHost6: Рис. 3.15. Панель управления Windows XP в режиме по категориям: пример списка с нестандартным размещением Оформление и темы Принтеры и другое оборудование Сеть и подключения к Интернету Учетные записи пользователей Установка и удаление программ Дата, время, язык и региональные стандарты Звук, речь и аудиоустройства Специальные возможности Производительность и обслуживание Центр обеспечения безопасности Рис. 3.16. Элемент управления ListBox с сеточным размещением 6 В шаблоне по умолчанию для ListBox в качестве владельца элементов списка используется класс ItemsPresenter, который согласован со свойством ItemsPanel. Глава 3. Элементы управления 142 Pick a category Оформление и темы Принтеры и другое оборудование Сеть и подключения к Интернету Учетные записи пользователей Установка и удаление программ Дата, время, язык и региональные стандарты Звук, речь и аудиоустройства Специальные возможности Производительность и обслуживание Центр обеспечения безопасности Рис. 3.17. С помощью шаблона ControlTemplate можно настроить не только способ размещения элементов списка <ControlTemplate TargetType=’{x:Type ListBox}’> <Border BorderBrush=’Black’ BorderThickness=’1’> <!— other elements —> <UniformGrid IsItemsHost=’True’ Columns=’2’ /> </Border> </ControlTemplate> Детализировав шаблон, мы сможем еще ближе подойти к виду панели управ# ления (рис. 3.17). Шаблоны – это замечательный способ настроить внешний вид списка. Когда накопится достаточно много сложных шаблонов, вы, наверное, захотите собрать их в пакет, чтобы можно было использовать в других приложениях. Шаблоны прекрасно работают до тех пор, пока не возникает необходимость ассоциировать с ними новые свойства. Например, если бы мы захотели создать шаблон для по# каза списка в несколько колонок, понадобилось бы место для хранения информа# ции о колонках. Несложно определить новый элемент управления, производный от ListBox, и включить в него дополнительные данные. Другой способ решить ту же задачу – воспользоваться элементом ListView, в который уже встроены необходимые средства. Элемент управления ListView Класс ListView, являясь производным от ListBox, добавляет к нему возможность отделить свойства представления (например, информацию о колонках при выводе в виде таблицы) от управляющих свойств (например, информации о том, какой эле# мент списка выбран). Так, мы можем вывести список в виде, показанном на рис. 3.18. Для этого понадобится определить объекты, в которых будут храниться данные: Библиотека элементов управления 143 class Person { string _name; bool _canCode; public Person(string name, bool canCode) { Name = name; CanCode = canCode; } public string Name { get { return _name; } set { _name = value; } } public bool CanCode { get { return _canCode; } set { _canCode = value; } } } В классе ListView нет свойств, позволяющих задать число колонок, заголовки и прочие аспекты представления. Чтобы их определить, мы должны присвоить свойству ListView.View в качестве значения объект типа GridView и уже для него установить все необходимые свойства. Для получения выводимых данных объек# ту GridView необходима привязка (подробнее эта тема рассматривается в главе 6). Рис 3.18. Отображение данных в элементе управления ListView с помощью встроенного объекта GridView <ListView> <ListView.View> <GridView> <GridView.Columns> <GridViewColumn Width=’300’ Header=’Name’ DisplayMemberBinding=’{Binding Path=Name}’ /> <GridViewColumn Width=’100’ Header=’CanCode’ Глава 3. Элементы управления 144 DisplayMemberBinding=’{Binding Path=CanCode}’ /> </GridView.Columns> </GridView> </ListView.View> </ListView> Такое разделение свойств на управляющие и относящиеся к отображению де# лает ListView самым развитым из списковых элементов. Чтобы создать новое представление для элемента ListView (нечто, способное заменить GridView), необходимо унаследовать обманчиво простому классу ViewBase. Суть требуемых действий проста: нужно переопределить метод PrepareItem и настроить свойства: public class MyView : ViewBase { private double angle; public double Angle { get { return angle; } set { angle = value; } } protected override void PrepareItem(ListViewItem item) { item.LayoutTransform = new RotateTransform(angle); base.PrepareItem(item); } } Но это лишь начало. По#настоящему полезным объект представления стано# вится за счет сочетания с системой стилизации. Переопределив свойство ItemContainerDefaultStyleKey, мы дадим этому объекту возможность задать все аспекты отображения, что и доказывает пример GridView. Рис. 3.19. Отображение иерархии списков в элементе TreeView Все списковые элементы, с которыми мы встречались до сих пор, были пред# назначены для отображения плоских списков. Однако часто бывает нужно предс# тавить иерархически организованные данные. Элемент управления TreeView Элемент TreeView добавляет возможность отображать иерархии, подобные приведенной на рис. 3.19. Можно считать, что дерево – это просто список, каж# дый элемент которого в свою очередь является списком. Именно так и работает Библиотека элементов управления 145 TreeView. Пожалуй, это самый лучший пример композиции; каждый объект TreeViewItem сам является элементом управления. Чтобы создать такое дерево, как на рис. 3.19, мы можем определить следующую иерархию: <TreeView> <TreeViewItem Header=’Coders’> <TreeViewItem Header=’Don’ /> <TreeViewItem Header=’Dharma’ /> </TreeViewItem> <TreeViewItem Header=’Noncoders’> <TreeViewItem Header=’Chris’ /> </TreeViewItem> </TreeView> Как и другие списковые элементы, TreeView может получать данные либо пу# тем добавления в свойство Items (как в примере выше), либо из свойства ItemsSource. О способе передачи иерархических данных элементу TreeView мож# но сказать немало интересного, но мы отложим этот разговор до главы 6. Создание новых списков с помощью шаблонов В самом начале обсуждения списков мы упомянули злополучный элемент RadioButtonList как пример специализированного элемента, который был изъят из WPF и заменен шаблоном. Список переключателей – это такой список, в ко# тором отдельные элементы и выбранный элемент представлены переключателя# ми (рис. 3.20). Реализация стиля, описывающего список переключателей, не потребует мно# го разметки, зато в ней используется довольно сложный синтаксис привязки к данным. Детально мы рассмотрим его ниже, а пока достаточно знать, что наша цель – привязать свойство IsChecked объекта RadioButton к свойству IsSelected объекта ListBoxItem: <ListBox> <ListBox.ItemContainerStyle> <Style TargetType=’{x:Type ListBoxItem}’> <Setter Property=’Template’> <Setter.Value> <ControlTemplate TargetType=’{x:Type ListBoxItem}’> <RadioButton IsChecked=’{Binding Path=IsSelected,RelativeSource={RelativeSource TemplatedParent}}’ Content=’{TemplateBinding Property=Content}’ /> </ControlTemplate> </Setter.Value> </Setter> </Style> </ListBox.ItemContainerStyle> <sys:String>Option 1</sys:String> 146 Глава 3. Элементы управления <sys:String>Option 2</sys:String> <sys:String>Option 3</sys:String> <sys:String>Option 4</sys:String> </ListBox> На этом примере еще только начинает проявляться вся сила системы шабло# нов вкупе с правильным выбором базовых классов. Меню и панели инструментов До сих пор я избегал упоминаний о двух очень интересных списковых эле# ментах управления: MenuItem и ToolBar. Хотя на первый взгляд это может показаться странным, но логически меню – не что иное, как элемент TreeView с очень специальным шаблоном. Взглянув на объектную модель, мы убежда# емся, что классы TreeViewItem и MenuItem наследуют одному и тому же ба# зовому классу HeaderedItemsControl. Хотя эти элементы управления приме# няются совсем не так, как стандартные списки, важно помнить, что и к меню, и к панелям инструментов в полной мере применимы модель содержимого и шаблоны. Меню и панели инструментов идут рука об руку. Они предоставляют по суще# ству одинаковую функциональность: возможность выполнить одну или несколь# ко команд. Основное различие между ними – это место на экране и модель взаи# модействия. Один и тот же набор команд на панели инструментов занимает боль# ше места, чем в меню, зато доступ к ним проще и быстрее. Как правило, на пане# ли инструментов размещаются альтернативные варианты уже имеющихся в ме# ню команд. Меню позволяют организовать глубокую иерархию и занимают ми# нимум места, однако найти в них нужную команду бывает нелегко. Обычно и ме# ню, и панели инструментов тесно связано с командами, но это вопрос мы будем обсуждать в главе 7. Меню Меню состоит из набора элементов MenuItem, погруженных либо в элемент Menu, либо в ContextMenu. В приложениях для Windows обычное меню распо# лагается, как правило, в верхней части окна. Контекстное же меню отображается по запросу пользователя, обычно путем щелчка правой кнопкой мыши или нажа# тия комбинации клавиш Shift+F10 (на современных клавиатурах имеется специ# альная клавиша для вызова контекстного меню). В User32 обычное меню принудительно располагалось сверху, но в WPF это ограничение снято. Теперь меню можно помещать где угодно. На рис. 3.21 пока# зано два способа отображения меню; добиться этого позволяют шаблоны. Одна# ко не забывайте о том, сколько времени у пользователя уйдет на привыкание к новой модели взаимодействия. В большинстве приложений лучше поместить ме# ню там, где пользователь ожидает его найти. Для создания меню мы включаем иерархию элементов MenuItem внутрь объ# екта Menu, который сам обычно погружен в элемент DockPanel, чтобы было про# ще отобразить меню в верхней части окна: Библиотека элементов управления Стандартное меню 147 Альтернативное меню Рис. 3.21. Различные представления меню <Window x:Class=’EssentialWPF.MenusAndToolbars’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’Menus and Toolbars’ > <DockPanel LastChildFill=’False’> <Menu DockPanel.Dock=’Top’> <MenuItem Header=’_File’> <MenuItem Header=’E_xit’ Click=’ExitClicked’ /> </MenuItem> <MenuItem Header=’_Edit’> <MenuItem Header=’_Cut’ /> <MenuItem Header=’C_opy’ /> <MenuItem Header=’_Paste’ /> </MenuItem> </Menu> </DockPanel> </Window> Взаимодействие с меню сводится к щелчку мышью по нужному пункту, в результате чего выполняются ассоциированные с этим пунктом действия. Ре# акция на щелчок программируется в обработчике события Click элемента MenuItem: <!— MenusAndToolbars.xaml —> ... <MenuItem Header=»E_xit» Click=»ExitClicked» /> ... // MenusAndToolbars.xaml.cs ... void ExitClicked(object sender, RoutedEventArgs e) { Close(); } ... Глава 3. Элементы управления 148 Эта программа выводит окно, показанное на рис. 3.22. Выглядит оно в точности, как мы и ожидаем: выпадающее меню, содержащее пункты, которые пользователь может вы# бирать машью. Выбор пункта File, а затем Exit приводит к завершению приложения. Тот факт, что класс MenuItem является производным от HeaderedItemsControl, теперь обретает смысл. И надпись «Edit» в заголовке меню, и все три дочерних пункта меню – объекты класса MenuItem. Класс ToolBar чуть более интересен. Панели инструментов Как и у меню, у панелей инструментов есть объемлющий контейнер – лоток (ToolBarTray), а сами они принадлежат типу ToolBar. Существенная разница между меню и панелью инструментов заключается в том, что меню поддержива# ет вложенность произвольного уровня, а панель – только один уровень. Рис. 3.22. Окно с простым меню Каждая панель имеет набор внутренних элементов и заголовок. В современ# ных приложениях заголовок, как правило, пуст, а элементы – это кнопки. Но мо# дель содержимого позволяет поместить на панель инструментов все что угодно: <Window x:Class=’EssentialWPF.MenusAndToolbars’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’Menus and Toolbars’ > <DockPanel LastChildFill=’False’> ... <ToolBarTray DockPanel.Dock=’Top’> <ToolBar> <Button>A</Button> <Button>B</Button> <Button>C</Button> <Button>D</Button> </ToolBar> <ToolBar Header=’Search’> <TextBox Width=’100’ /> <Button Width=’23’>Go</Button> Библиотека элементов управления 149 </ToolBar> </ToolBarTray> </DockPanel> </Window> Это приложение выводит окно, показанное на рис. 3.23. Панель можно переме# щать внутри лотка (но не за его пределы, где она превратилась бы в плавающую панель), как показанно на рис. 3.23. Кроме того, если уменьшить размер окна, то не помещающиеся элементы могут быть перенесены в дополнительное меню; уп# равляет этим свойство ToolBar.OverflowMode: Рис. 3.23. Две панели инструментов: одна с кнопками, другая с кнопкой и текстовым полем Рис. 3.24. Панель инструментов, часть которых находится в дополнительном меню 150 Глава 3. Элементы управления Рис. 3.25. Различные вложенные контейнеры Панели инструментов часто используются в качестве универсальных контей# неров для различных элементов управления, например, текстовых полей (к при# меру, для задания критерия поиска) или комбинированных списков (скажем, ад# ресной строки в большинстве Web#браузеров). В WPF также есть есть ряд кон# тейнеров общего назначения. Контейнеры Для группирования и сокрытия частей пользовательского интерфейса в WPF есть три основных элемента управления: TabControl, Expander и GroupBox. TabControl реализует ставшие уже привычными вкладки, Expander предлагает возможность свертывания и развертывания в стиле Windows XP, которую мож# но видеть в обозревателе файловой системы, а GroupBox – простой визуальный способ объединить логически связанные части пользовательского интерфейса. Все они показаны на рис. 3.25: <TabControl> <TabItem Header=’Page 1’> <StackPanel> <Expander Header=’Section 1’ IsExpanded=’True’> <GroupBox Header=’Subsection A’> <Label>Some Content!</Label> </GroupBox> </Expander> <Expander Header=’Section 2’> <GroupBox Header=’Subsection A’> <Label>Some Content!</Label> </GroupBox> </Expander> </StackPanel> </TabItem> <TabItem Header=’Page 2’ /> </TabControl> Об иерархии наследования этих элементов управления стоит сказать особо. Во# первых, все три класса TabItem, Expander и GroupBox наследуют Headered Библиотека элементов управления 151 ContentControl. Последний является обобщенным базовым классом для любого эле# мента управления, модель данных которого предусматривает один заголовок и оди# ночное содержимое. На первый взгляд это утверждение может показаться непра# вильным, так как и TabItem, и Expander, и GroupBox чаще всего содержат несколько элементов. Но это только потому, что они погружены в один менеджер размещения, например, StackPanel или Grid, который служит корнем для всего содержимого. Еще одно интересное наблюдение связано с тем, что TabControl наследует классу Selector (и, следовательно, ItemsControl). Это означает, что TabControl на самом деле является списковым типом, а, стало быть, к нему применимы модель данных, система шаблонов и события выбора, ассоциированные со списками. Последняя группа контейнерных элементов состоит из менеджеров размеще# ния, таких, как WrapPanel. Их мы будем рассматривать в главе 4. Диапазоны Диапазонные элементы управления позволяют выбирать значение между верхней и нижней границей (диапазоном допустимых значений, откуда и назва# ние). В WPF есть три таких элемента: Slider, ScrollBar и ProgressBar. ScrollBar считается вспомогательным элементом, поскольку использовать его для чего#ли# бо, кроме прокрутки, – признак неудачного пользовательского интерфейса. Все диапазонные элементы работают одинаково: с помощью свойств Minimum и Maximum задаются нижняя и верхняя границы диапазона, а с помощью свойства Value задается или определяется текущее значение внутри диапазона. Можно также задать свойства SmallChange и LargeChange, которые управляют тем, насколько быст# ро изменяется значение, когда пользователь щелкает по различным частям элемента. Чтобы продемонстрировать работу ползунка (slider), мы можем в обработчи# ке его события ValueChanged обновить содержимое текстового блока: <!— RangeControls.xaml —> <Window x:Class=’EssentialWPF.RangeControls’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’Range Controls’ SizeToContent=’WidthAndHeight’ > <StackPanel Orientation=’Horizontal’ Margin=’5’> <TextBlock Name=’_value’ Margin=’10’ /> <Slider Name=’_slider’ Width=’75’ Minimum=’0’ Maximum=’255’ Value=’255’ ValueChanged=’SliderChanged’ /> </StackPanel> </Window> Глава 3. Элементы управления 152 // RangeControls.xaml.cs public partial class RangeControls : Window { public Window1() { InitializeComponent(); } void SliderChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { _value.Text = _slider.Value.ToString(); } } Эта программа выводит окно, показанное на рис. 3.26. Пользователь может из# менять текст, перемещая ползунок. Ползунок также часто применяют для изме# нения масштаба части пользовательского интерфейса. Подписавшись на уведом# ление об изменениях (а еще лучше, воспользовавшись привязкой к данным), мы можем без труда добавить функцию управляемого пользователем масштабирова# ния в любое приложение. Редакторы В WPF есть несколько редакторов: PasswordBox, TextBox, RichTextBox и InkCanvas. Рис. 3.26. Ползунок Элемент PasswordBox стоит наособицу, поэтому рассмотрим его первым. Это хорошо знакомое текстовое поле, в котором вместо вводимых пользователем символов отображаются точки или звездочки. Его реализация интересна тем, что фактически не поддерживает объектную модель, характерную для текста. PasswordBox – это изолированный островок функциональности, реализующий скрытое хранение пароля внутри себя. Уровень защищенности повышается за счет того, что обход дерева текста не позволит случайно узнать пароль. Элементы TextBox и RichTextBox очень похожи, только в TextBox отключена возможность хранить и отображать обогащенный текст, а вместо нее предлагается много «простых» средств для редактирования текста (максимальная длина, выбор регистра и т.д.). Поскольку TextBox поддерживает только обычный текст, его API гораздо проще, так что для простых случаев не нужно осваивать объектную мо# дель текста. Что же касается поддержки метода ввода (IME – input method editor), Библиотека элементов управления 153 проверки правописания (да, такая возможность встроена во все текстовые редак# торы!) и других платформенных средств, то они в обоих элементах одинаковы. RichTextBox – это патриарх среди текстовых редакторов. Без какого бы то ни было дополнительного программирования он поддерживает 84 команды, доступ# ные с помощью клавиатуры (вы знали, что комбинации клавиш Ctrl+[ и Ctrl+] изменяют размер шрифта в выбранном фрагменте?), и еще несколько десятков – из программы. По существу, элемент RichTextBox представляет собой редактор типа FlowDocument, о котором мы расскажем в главе 5. Элемент InkCanvas по отношению к рукописному вводу – то же, что RichTextBox по отношению к тексту. По умолчанию он поддерживает ввод, сти# рание, выделение и «жесты»7 без дополнительного программирования. У редакторов рукописного ввода и текста имеются соответствующие объект# ные модели для инспектирования и модификации платформенных типов данных. Текстовые данные В WPF есть два способа работы с текстовыми данными: потоки и наборы. В большинстве случаев, когда приходится динамически строить и инспектировать текст, пользоваться наборами проще. Но для редактирования обогащенного текс# та (и выбора фрагмента в нем) необходимо понимать некоторые базовые аспекты потоковой модели текста. Модель наборов для манипуляций с текстом работает так же, как все прочие встречавшиеся нам раньше управляющие свойства: мы конструируем элементы текста и добавляем их в те или иные наборы. FlowDocument document = new FlowDocument(); Paragraph para = new Paragraph(); para.Inlines.Add(new Run(«Hello World»)); document.Blocks.Add(para); Обратите внимание, что у объекта FlowDocument есть свойство Blocks, а у объекта Paragraph – свойство Inlines. Все множество элементов текста разбито на две больших категории: блочные и встроенные. Блочный элемент занимает на эк# ране прямоугольник, он начинается с новой строки и продолжается без разрывов. Иными словами, блочные элементы ведут себя как все прочие элементы WPF с тем исключением, что могут пересекать границы страниц. Примерами блочных элементов могут служить Paragraph или Table. Встроенные элементы могут за# нимать несколько строк. К их числу относятся, например, Span, Run и Bold. Объектная модель текста налагает жесткие правила на вложенность элемен# тов, чтобы обеспечить предсказуемое поведение редакторов. Эти правила доволь# но просты: 1. Run – единственный элемент, который может содержать текстовые строки (собственно, содержимое). 7 Понятие жеста пришло из TabletPC и означает преобразование «движения» (например, рисова) ние галочки или окружности) в программную команду (например, «отметить» или «окружность»). Распознавание жестов ) это частный случай распознавания рукописного ввода. Глава 3. Элементы управления 154 2. Paragraph – единственный блочный элемент, который может содержать встроенные элементы (причем он не может содержать ничего, кроме встро# енных элементов). 3. Table содержит один или несколько объектов TableRowGroup, каждый из которых содержит один или несколько объектов TableRow, а каждый из них – один или несколько объектов TableCell. Только TableCell может со# держать блочные элементы. 4. List содержит один или несколько объектов ListItem, которые могут содер# жать блочные элементы. 5. Блочные контейнеры (Section и т.д.) могут содержать только другие блоч# ные элементы (по существу, это повторение правила 2). 6. Встроенные контейнеры (Span, Bold и т.д.) могут содержать только встро# енные элементы (по существу, это повторение правила 1). На рис. 3.27 эти правила проиллюстрированы. Документ Встроенные элементы Блочные элементы Встроенные контейнеры (Span, Bold и т.д.) Table Run Table Row Group Строка текста Table Row List Блочные контейнеры Table Cell List Item Section, etc Paragraph Рис. 3.27. Абстрактная модель текста Модель текста на базе наборов напрямую следует этим правилам. У некото# рых объектов есть вспомогательные методы (например, у объекта Paragraph есть свойство Text типа string), которые создают нужные элементы. Есть еще два интересных текстовых элемента: BlockUIContainer и InlineUIContainer. Они служат контейнерами для объектов любого класса, про# Библиотека элементов управления 155 изводного от UIElement, которые включаются в поток текста, точно так же, как TextFlow и RichTextBox являются контейнерами для текстового содержимого, хранящегося в дереве объектов UIElement. В главе 5 мы подробно рассмотрим, что делают все эти текстовые элементы. До сих мы говорили об объектной модели текста, в основе которой лежат наборы. Потоковая модель применяется для манипулирования текстом как потоком элементов. Важно по# нимать, что текст по природе своей последователен. Возможно, эта мысль звучит три# виально, но при проектированиии WPF вопрос оказался довольно сложным. Обычно в графических библиотеках (User32, Windows Forms, Abstract Window Toolkit [AWT], Swing и т.д.) объекты представлены в виде дерева элементов. У каждого элемента есть родитель и, возможно, потомки. Такая модель проста для понимания. Напротив, в библиотеках для работы с текстом или документами (например, в Internet Explorer) объекты представлены в виде потока текста. Поскольку ос# новным строительным блоком в такой библиотеке является поток символов, то все остальное может быть к такому потоку приведено. Подумайте, как должна была бы выглядеть объектная модель, способная представить такой, вполне кор# ректный, фрагмент HTML: <b>hello <i>there</b> reader</i>. Перед командой разработчиков WFF встал вопрос: как представлять объек# ты? Мы понимали, что развитые типографические средства и документы должны стать одной из базовых частей платформы, но рассматривать текст исключитель# но как поток символов означало бы проигрыш в производительности и увеличе# ние сложности. Кроме того, предполагалось, что платформа будет обладать раз# витым каркасом для работы с элементами управления, а выразить концепцию выделения фрагмента в терминах простой древовидной модели нелегко. Поэтому мы решили взять на вооружение оба подхода. При добавлении в дерево текста ис# пользуется потоковая модель (отсюда объектная модель текста), а для элементов управления – простая иерархия (с ней мы уже встречались). Основными кирпичиками в объектной модели потокового текста являются объекты класса TextPointer. Указатель текста можно считать перстом, указую# щим в место, расположенное между двумя символами. Такой указатель может представлять позицию каре или начало и конец выделенного фрагмента или на# чало слова. Объект TextRange используется для представления всего выделенного фраг# мента; он состоит из двух указателей текста8. Форматирование и визуальные эф# фекты можно применять к произвольным диапазонам. Это очень важно, поэтому позволю себе еще раз повторить: форматирование применимо к произвольным ди& апазонам. С помощью диапазонов можно смоделировать и тот неприятный при# мер с перекрывающимися участками полужирного и курсивного форматирова# ния, который был приведен выше. Однако при попытке сохранить сгенерирован# ную разметку обнаружится, что перекрывающиеся теги были удалены, а вместо них создано нечто более разумное, а именно: 8 Объекты TextRange пригодны также для сохранения и загрузки участков обогащенного формати) рованного текста. Метод TextRange.Save позволяет записать весь документ или его часть в фор) мате RTF или XAML, а метод TextRange.Load — прочитать данные в любом из этих форматов как целый документ или его часть. Глава 3. Элементы управления 156 <Bold>hello </Bold><Inline FontWeight=’Bold’ FontStyle=’Italic’> there</Inline><Italic>reader</Italic>. Свойство Xml объекта TextRange возвращает очень детальную XAML#размет# ку, в которой представлена вся информация (в том числе и умолчания) о тексто# вом диапазоне. Она полезна для операций с буфером обмена (Clipboard) и в дру# гих случаях, когда надо извлечь часть документа. Рис. 3.28. Элемент RichTextBox, в котором выделен фрагмент текста Элемент RichTextBox Работу с элементом RichTextBox следует начать со свойства Document. Этот элемент позволяет редактировать один объект типа FlowDocument. Пользуясь объектной мо# делью текста, мы можем инициализировать RichTextBox форматированным текстом: <Window ... Title=’RichTextBox’> <RichTextBox Name=’_rtb’> <FlowDocument FontSize=’24’> <Paragraph>Hello</Paragraph> </FlowDocument> </RichTextBox> </Window> Во время выполнения программы мы получаем полнофункциональный ре# дактор (рис. 3.28), который поддерживает все стандартные команды. Чтобы эффективно работать с элементом RichTextBox, нужно ясно понимать, что такое каре и выделенный фрагмент. Каре – это мигающая вертикальная ли# ния, которая представляет текущую позицию вставки и в общем случае является началом выделенного фрагмента. Для отслеживания позиций каре и выделен# ных фрагментов используются три объекта TextPointer: CaretPosition, Selection.Start и Selection.End. Библиотека элементов управления 157 Рис. 3.29. Пример объекта TextOffset, показывающий, какое место в объектной модели текста занимают начальная и конечная лексемы, описывающие элемент Рис. 3.30. Упрощенная разметка текста; показаны смещения каждого элемента Работа с объектами TextPointer не всегда интуитивно очевидна, главным образом, потому что они раскрывают детали механизма хранения текста. Когда приходится мани# пулировать выделенным фрагментом из программы, об этих деталях тоже нужно кое#что знать. Начальные и конечные элементы в хранящемся тексте представлены отдельными лексемами. Это означает, что метод GetPositionAtOffset будет видеть эти лексемы: public class TextOffset : Window { RichTextBox _rtb = new RichTextBox(); public TextOffset() { Content = _rtb; _rtb.SelectionChanged += new RoutedEventHandler(Changed); } void Changed(object sender, RoutedEventArgs e) { Title = _rtb.Document.ContentStart.GetOffsetToPosition( _rtb.CaretPosition).ToString(); } } Запустив эту программу (рис. 3.29), мы сможем ввести символы и с помощью комбинации клавиш Ctrl+B выделить два средних символа полужирным шриф# том. (На рис. 3.30 показаны смещения каждого элемента.) Если поместить каре перед буквой a, то окажется, что смещение равно 2. Объясняется это тем, что бук# ве a предшествуют элементы Section и Run. Продвигаясь направо, мы получим такую последовательность смещений: 2, 3, 4, 7, 8, 11, 129. Перепрыгивания (от 4 к 9 При продвижении справа налево мы будем видеть другие числа. Почему? Потому что в дело вступает концепция притяжения (gravity). Система пытается угадать, в какой элемент мы со) бираемся вставить текст. Если каре находится между буквами с и d, и мы двигаемся вправо, то, вероятно, текст надо вставить внутрь элемента, выделенного полужирным шрифтом. Если же каре находится между e и f, а мы двигаемся влево, то, наверное, имеется в виду вставка текста внутрь невыделенного элемента. Идея притяжения находит широкое применение в текстовых редакторах, а пользователю, который имеет дело с таким редактором, просто ка) жется, что "все работает". 158 Глава 3. Элементы управления 7 и от 8 к 11) происходят сразу после букв b и d. Это связано с наличием конеч# ного и последующего начального элемента. Рис. 3.31. Некорректно реализованный поиск При работе с элементом RichTextBox смещения важны, потому что их нужно учитывать при перемещении каре или выделенного фрагмента. При первой по# пытке написать простой метод поиска для RichTextBox я упустил это из виду (рис. 3.31): <!— RichTextBoxWork.xaml —> <Window x:Class=’EssentialWPF.RichTextBoxWork’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’RichTextBox’ > <DockPanel> <WrapPanel DockPanel.Dock=’Top’ > <TextBox Width=’100’ Name=’_toFind’ /> <Button Click=’Find’>Find</Button> <TextBlock Name=’_offset’ /> </WrapPanel> <RichTextBox Name=’_rtb’ /> </DockPanel> </Window> // richtextboxwork.xaml.cs public partial class RichTextBoxWork : Window { public RichTextBoxWork() { InitializeComponent(); } Библиотека элементов управления 159 // Это код неправилен! // void Find(object sender, RoutedEventArgs e) { FlowDocument doc = _rtb.Document; string text = new TextRange(doc.ContentStart, doc.ContentEnd).Text; int index = text.IndexOf(_toFind.Text); TextPointer start = doc.ContentStart.GetPositionAtOffset(index); TextPointer end = start.GetPositionAtOffset(_toFind.Text.Length); _rtb.Selection.Select(start, end); } } В этом примере выделения никогда не будет из#за начальных лексем элементов Section и Run. Чтобы исправить ошибку, нужно перемещать указатель текста по документу во время поиска: void Find(object sender, RoutedEventArgs e) { FlowDocument doc = _rtb.Document; TextPointer cur = doc.ContentStart; while (cur != null) { TextPointer end = cur.GetPositionAtOffset(_toFind.Text.Length); if (end != null) { TextRange test = new TextRange(cur, end); if (test.Text == _toFind.Text) { _rtb.Selection.Select(test.Start, test.End); break; } } cur = cur.GetNextInsertionPosition(LogicalDirection.Forward); } } И последнее, о чем надо знать при работе с элементом управления RichTextBox, – это понятие отмены операции. Отменить (и повторить) можно лю# бую операцию. Единицы отмены также организованы иерархически, то есть мож# но создать группу операций, которая будут считаться одним изменением. Для этой цели предназначены методы BeginChange/EndChange или DeclareChangeBlock; но в общем случае DeclareChangeBlock предпочтительнее, так как он возвращает объект IDisposable, который можно заключить в блок using. Рассмотрим следующую программу для добавления в обогащенный текст двух абзацев. Если запустить ее, то для удаления обоих абзацев, пользователю придется дважды выполнить отмену: _rtb.Document.Blocks.Add(new Paragraph(new Run(«One»))); _rtb.Document.Blocks.Add(new Paragraph(new Run(«Two»))); Вместо этого можно заключить все программные изменения документа в блок, который будет считаться одной единицей отмены: 160 Глава 3. Элементы управления using (_rtb.DeclareChangeBlock(«add two»)) { _rtb.Document.Blocks.Add(new Paragraph(new Run(«One»))); _rtb.Document.Blocks.Add(new Paragraph(new Run(«Two»))); } При внесении изменений в содержимое элемента RichTextBox лучше оберты# вать их в объект DeclareChangeBlock, чтобы пользователю было проще в случае чего выполнить отмену всех операций разом. Элемент TextBox Элемент TextBox предлагает упрощенную реализацию большинства возмож# ностей RichTextBox. Так как TextBox поддерживает только один формат, то вы# деленный фрагмент можно описать двумя целыми числами – смещениями от на# чала текста (начальная и конечная лексемы не нужны), а сам текст – одним стро# ковым значением. В довесок к этому упрощению, TextBox предлагает и некото# рые дополнительные операции, например, возможность ограничить длину текста или изменять регистр букв. Рукописный ввод Для рукописного ввода базовым типом данных является класс Stroke, опреде# ленный в пространстве имен System.Windows.Ink. В некоторых отношениях ру# кописные данные намного проще обогащенного текста; по существу, это не более чем последовательности пакетов, получаемых от дигитайзера. Рукописный ввод был впервые включен в версию Windows XP для Tablet PC (если не вспоминать о мифических версиях Pen Windows). Для него разработан двоичный формат хранения (ISF – ink serialized format) и модели программиро# вания для COM и .NET. При разработке WPF мы хотели полностью интегриро# вать рукописный ввод в состав платформы. Двоичный формат почти не изменил# ся, но модель программирования все же была немного модифицирована для луч# шей увязки с остальными частями платформы. О двоичном формате можно вообще не говорить без ущерба для понима# ния. Достаточно знать, что объектная модель Stroke реализована поверх очень эффективного двоичного потока. Эта деталь объясняет некоторые странности в ее интерфейсе. В большинстве случаев все необходимые дан# ные можно с помощью простого StylusPoint API, но, если нужен доступ ко всем данным, то к вашим услугам более развитый StylusPointDescription API 10. Чтобы продемонстрировать работу с рукописным вводом, мы вос# пользуемся элементом управления InkCanvas и будем обрабатывать собы# тие InkCollected (рис. 3.32): 10 Для запуска этого примера вам не потребуется Tablet PC. По умолчанию при работе с элементом управления InkCanvas WPF интерпретирует мышь, как перо. Современные мыши не посылают дан) ных о давлении на поверхность, поэтому все линии будут одной толщины, но в конце концов мож) но ведь и убедить начальника приобрести для вас Tablet PC, правда? Библиотека элементов управления 161 Рис. 3.32. Работа с объектной моделью рукописного ввода <!— inktest.xaml —> <Window ... x:Class=’EssentialWPF.InkTest’ Title=’Ink Test’ Visibility=’Visible’ > <Grid> <InkCanvas Name=’_ink’ StrokeCollected=’Collected’ Background=’Beige’ /> <Canvas Name=’_overlay’ /> </Grid> </Window> // inktest.xaml.cs void Collected(object sender, InkCanvasStrokeCollectedEventArgs e) { _overlay.Children.Clear(); Brush fill = new SolidColorBrush(Color.FromArgb(120, 255, 0, 0)); foreach (StylusPoint pt in e.Stroke.StylusPoints) { double markerSize = pt.PressureFactor * 35.0; Ellipse marker = new Ellipse(); Canvas.SetLeft(marker, pt.X markerSize / 2); Canvas.SetTop(marker, pt.Y markerSize / 2); marker.Width = marker.Height = markerSize; marker.Fill = fill; Глава 3. Элементы управления 162 _overlay.Children.Add(marker); } } С каждым штрихом (stroke) ассоциированы три набора данных: пакеты диги# тайзера (они называются пакетами стилоса), атрибуты рисования и данные, оп# ределяемые приложением. Последние позволяют приложениям, оптимизирован# ным для рукописного ввода, снабжать поступающие данные дополнительной ин# формацией. Атрибуты рисования управляют способом отображения рукописно# го ввода. Задавать атрибуты рисования для каждого штриха позволяет свойство DrawingAttributes. Можно также с помощью свойства DefaultDrawingAttributes объекта InkCanvas задать атрибуты по умолчанию. Рис. 3.33. Изменения отображения с помощью дополнительных свойств из объектной модели рукописного ввода Как вы, наверное, уже начали осознавать, пакеты стилоса устроены доволь# но сложно. Причина в том, что разные производители могут расширять поток рукописного ввода. Так, производители дигитайзеров очень часто вводят до# полнительные кнопки. Другой пример – данные об угле наклона пера и даже о его вращении. Конечная цель – собрать всю возможную информацию о манере Библиотека элементов управления 163 письма или рисования конкретного человека, чтобы цифровой результат был не хуже или даже лучше получающегося при использовании обычного пера и бумаги. Чтобы получить дополнительные данные, мы должны сообщить объектной модели рукописного ввода, какие данные должны передаваться из пакетов в мо# дель. Можно запрашивать данные глобально (задав свойство DefaultStylusPoint Description объекта InkCanvas) или для каждого штриха (с помощью метода Reformat объекта StylusPointCollection). Многие современные планшеты поддерживают свойства XTilt и YTilt (углы наклона в двух направлениях), которые открывают возможность для очень инте# ресных визуализаций. Но в примере ниже я ограничусь кнопкой BarrelButton, имеющейся в большинстве планшетов. Первыми тремя свойствами в запросе должны быть X, Y и NormalPressure. Если кнопка пера нажата, мы рисуем окруж# ности по#другому (рис. 3.33): public InkTest() { InitializeComponent(); _ink.DefaultStylusPointDescription = new StylusPointDescription( new StylusPointPropertyInfo[] { new StylusPointPropertyInfo(StylusPointProperties.X), new StylusPointPropertyInfo(StylusPointProperties.Y), new StylusPointPropertyInfo(StylusPointProperties.NormalPressure), new StylusPointPropertyInfo(StylusPointProperties.BarrelButton), }); } void Collected(object sender, InkCanvasStrokeCollectedEventArgs e) { _overlay.Children.Clear(); Brush fill = new SolidColorBrush(Color.FromArgb(120, 255, 0, 0)); Brush altFill = new SolidColorBrush(Color.FromArgb(120, 0, 255, 0)); foreach (StylusPoint pt in e.Stroke.StylusPoints) { double markerSize = pt.PressureFactor * 35.0; Ellipse marker = new Ellipse(); Canvas.SetLeft(marker, pt.X markerSize / 2); Canvas.SetTop(marker, pt.Y markerSize / 2); marker.Width = marker.Height = markerSize; marker.Fill = fill; if (pt.GetPropertyValue(StylusPointProperties.BarrelButton) != 0) { marker.Fill = null; marker.Stroke = Brushes.Black; marker.StrokeThickness = 2.0; } _overlay.Children.Add(marker); } } Глава 3. Элементы управления 164 Элемент управления InkCanvas С основами использования элемента InkCanvas мы познакомились, но это далеко не все. Элемент RichTextBox обладает всей функциональностью, необ# ходимой для реализации редактора WordPad, включенного в дистрибутив Windows. А с помощью InkCanvas мы можем реализовать приложение Journal. Все необходимое уже имеется: выделение фрагмента, стирание, подс# вечивание, описание советов и т.д. Пожалуй, наиболее интересна поддержка жестов. Распознаватель жестов анализирует рукописные данные и определяет, не пы# тался ли пользователь ввести какой#нибудь из 41 жеста (включены все, от галоч# ки (Check) до двойной завитушки (DoubleCurlicue)). Чтобы включить режим распознавания жестов, нужно выполнить два важных шага: во#первых, присвоить свойству EditingMode объекта InkCanvas значение InkAndGesture, а, во#вторых, вызвать метод SetEnabledGestures объекта InkCanvas, чтобы сообщить распозна# вателю, какие объекты искать: <!— InkCanvasTest.xaml —> <Window ... x:Class=’EssentialWPF.InkCanvasTest’ Title=’EssentialWPF’ > <StackPanel> <InkCanvas Height=’200’ Name=’_ink’ Gesture=’InkGesture’ EditingMode=’InkAndGesture’ /> <ListBox Name=’_seen’ /> </StackPanel> </Window> // InkCanvasTest.xaml.cs public partial class InkCanvasTest : Window { public InkCanvasTest() { InitializeComponent(); _ink.SetEnabledGestures(new ApplicationGesture[] { ApplicationGesture.AllGestures, }); } void InkGesture(object sender, InkCanvasGestureEventArgs e) { _seen.Items.Add( e.GetGestureRecognitionResults()[0].ApplicationGesture); } } Запустите эту программу и попробуйте несколько разных жестов (рис. 3.34), чтобы понять, насколько ваш почерк близок к тому, что понимает распознава# тель. Жесты – это прекрасный способ включить поддержку рукописного ввода в приложение. Библиотека элементов управления 165 Рис. 3.34. Распознавание жестов в элементе управления InkCanvas Средства просмотра документов В WPF есть несколько способов отображения документов. Самые простые – элемент FlowDocumentScrollViewer (просмотр с прокруткой без возможности редактирования) и RichTextBox (просмотр с прокруткой и редактированием). Но чтобы пользователю было удобнее читать документы, платформа предлагает еще несколько вариантов. По существу, строительных блоков всего два: FlowDocumentScrollViewer и FlowDocumentPageViewer. Первый представляет собой средство просмотра с прокруткой и стандартным управлением (панорамирование и т.д.). Второй дает возможность просматривать документ постранично и имеет привычные навига# ционные элементы. Но лучше всего элемент управления FlowDocumentReader, который позволяет пользователю самостоятельно выбрать способ представления: одностраничный, многостраничный или с прокруткой. На рис. 3.35 показаны все три элемента. Средства просмотра документа – это один вид контейнера для содержимого, в данном случае документов. Другим является фрейм. Глава 3. Элементы управления 166 Элемент управления Frame Элемент Frame позволяет разместить содержимое с автономной навигацией в любом месте окна. Существуют две интересные модели размещения содержимо# го с возможностью навигации: изолированная и интегрированная. В случае изолированной модели считается, что содержимому нельзя дове# рять, поэтому оно исполняется в полностью автономном окружении (песочнице). Именно так WPF размещает содержимое при работе в системном Web#браузере, например, XAML Browser Application. Для перехода в другое приложение или к другому HTML#содержимому в изолированной модели размещения предусмот# рен объект Frame. Рис. 3.35. Все представления документа. Отметим, что у FlowDocumentReader имеется элемент управления просмотром, который позволяет выбрать страничный режим или режим прокрутки. Также он поддерживает поиск в документе. Интегрированная модель размещения, в которой содержимое ведет себя как часть приложения, в системе не поддерживается вовсе. Переход к другому содер# жимому внутри приложения с помощью объекта Frame можно рассматривать как своеобразный гибрид изолированного и интегрированного поведения. Фрейм изолирует свое содержимое от собственного (и родительского) стиля, но не от стиля приложения. События не распространяются за пределы фрейма, однако объекты в нем доступны через свойство Content (то есть с точки зрения безопас# ности изолированными не являются). Таким образом, объект Frame особенно полезен для работы с внешним содер# жимым, но при желании его можно применять и для вывода содержимого самого приложения. Библиотека элементов управления 167 Строительные блоки Модель элементов управления в WPF основана на идее композиции. Круп# ные элементы (например, ListBox) составляются из более мелких (StackPanel, ScrollViewer и т.д.). В пространстве имен System.Windows.Controls.Primitives есть немало мелких компонентов, из которых строятся элементы управления. ToolTip В общем случае доступ к всплывающей подсказке (tool tip) дает свойство ToolTip, имеющееся у любого элемента управления: <Button ToolTip=’Click this button to do cool things!’> OK </Button> Есть два стандартных способа изменить поведение подсказок. Во#первых, служба ToolTipService позволяет настроить большее число свойств. Ее достоин# ства в том, что создается лишь один объект ToolTip (это полезно, если требуется много подсказок для разных элементов управления). В более сложных случаях можно создать объект ToolTip и ассоциировать его с конкретным элементом: <!— ToolTipTest.xaml —> <Window ... x:Class=’EssentialWPF.ToolTipTest’ Title=’Tool Tips’ ToolTipService.InitialShowDelay=’0’ ToolTipService.ShowDuration=’500000’ > <Window.ToolTip> <ToolTip x:Name=’_theTip’ Placement=’RelativePoint’ VerticalOffset=’10’ /> </Window.ToolTip> <UniformGrid Rows=’3’ Columns=’3’> <Button Margin=’5’>1</Button> <Button Margin=’5’>2</Button> <Button Margin=’5’>3</Button> <Button Margin=’5’>4</Button> <Button Margin=’5’>5</Button> <Button Margin=’5’>6</Button> <Button Margin=’5’>7</Button> <Button Margin=’5’>8</Button> <Button Margin=’5’>9</Button> </UniformGrid> </Window> // ToolTipTest.xaml.cs public partial class ToolTipTest : Window { public ToolTipTest() { InitializeComponent(); Глава 3. Элементы управления 168 this.MouseMove += ToolTipTest_MouseMove; } void ToolTipTest_MouseMove(object sender, MouseEventArgs e) { // Ищется элемент, над которым находится мышь. // PointHitTestResult hit = VisualTreeHelper.HitTest( this, e.GetPosition(this)) as PointHitTestResult; if (hit != null && hit.VisualHit != null) { _theTip.Content = hit.VisualHit.ToString(); _theTip.PlacementRectangle = new Rect(e.GetPosition(this), new Size(0, 0)); } } } Рис. 3.36. Элемент ToolTip в действии Запустив эту программу (рис. 3.36), мы никогда не сможем попасть мышью в са# мо окно. Так как подразумеваемый по умолчанию шаблон для элемента Window включает рамку вокруг объекта ContentPresenter, служащего контейнером для со# держимого, система никогда не скажет, что щелчок мышью произошел в самом ок# не11. То же самое справедливо и для кнопки Button: на элемент ButtonChrome и на текстовый блок навести мышь можно, а на саму кнопка – нет. Это еще один довод в подтверждение того, что у элементов управления нет собственного отображения. Конечно, можно создать специальный шаблон для элемента управления ToolTip, придав ему желаемую форму и внешний вид (рис. 3.37). 11 На самом деле, в WPF вообще нет API для доступа к самым внешним элементам окна (полоса заго) ловка, кнопки свертывания и развертывания и т.д.). Чтобы изменить их (или получать от них собы) тия), нужно либо воспользоваться технологией вызова системных API (P/Invoke), либо отключить стандартный механизм изображения окна (WindowsStyle.None) и рисовать рамку самостоятельно. Библиотека элементов управления 169 Thumb Элемент Thumb описывает область, которую можно перемещать. Точнее, он позволяет получать события, когда пользователь перетаскивает эту область (thumb, манипулятор). С его помощью можно легко создать манипуляторы для изменения размеров или подвижные разделители (splitter). Рис. 3.37. Элемент ToolTip с шаблоном, обеспечивающим полупрозрачность В следующем примере мы просто перемещаем манипулятор, когда пользова# тель тянет за него. В событии DragStarted запоминается начальное положение, а в событии DragDelta можно переместить манипулятор на подходящее расстояние: <!— window1.xaml —> <Window ... x:Class=’EssentialWPF.ThumbSample’ Text=’Thumb’ > <Canvas> <Thumb Canvas.Top = ‘5’ Canvas.Left = ‘5’ Width = ‘10’ Height = ‘10’ Name=’_thumb1’ DragStarted=’ThumbStart’ DragDelta=’ThumbMoved’ /> </Canvas> </Window> // window1.xaml.cs ... double _originalLeft; double _originalTop; void ThumbStart(object sender, DragStartedEventArgs e) { Глава 3. Элементы управления 170 _originalLeft = Canvas.GetLeft(_thumb1); _originalTop = Canvas.GetTop(_thumb1); } void ThumbMoved(object sender, DragDeltaEventArgs e) { double left = _originalLeft + e.HorizontalChange; double top = _originalTop + e.VerticalChange; Canvas.SetLeft(_thumb1, left); Canvas.SetTop(_thumb1, top); _originalLeft = left; _originalTop = top; } ... Border Border – это совсем простой и тем не менее полезный элемент для создания визуальных эффектов. Представляет он собой прямоугольник, в котором может находиться дочерний элемент. Это удобно, потому что большинство элементов# рисовальщиков (Rectangle, Ellipse и т.д.) не могут содержать потомков. Типич# ный обходной путь состоит в том, чтобы поместить такой элемент в менеджер размещения (например, в Grid или Canvas). Но, поскольку обрамление элемен# та прямоугольником встречается весьма часто, для этой цели был разработан элемент Border. На самом деле, Border несколько больше, чем просто прямоугольник, он поз# воляет задавать переменную толщину сторон и радиус скругления углов. В ре# зультате получаются довольно необычные рамки (рис. 3.38): Рис. 3.38. Рамки, созданные путем задания различной толщины сторон и радиуса скругления углов <Canvas> <Border Canvas.Left=’15’ Canvas.Top=’15’ Библиотека элементов управления BorderThickness=’3’ CornerRadius=’0’ BorderBrush=’Black’ Padding=’5’> <TextBlock>Hello</TextBlock> </Border> <Border Canvas.Left=’85’ Canvas.Top=’15’ BorderThickness=’3’ CornerRadius=’3’ BorderBrush=’Black’ Padding=’5’> <TextBlock>Hello</TextBlock> </Border> <Border Canvas.Left=’15’ Canvas.Top=’50’ BorderThickness=’10,1,10,1’ CornerRadius=’10’ BorderBrush=’Black’ Padding=’5’> <TextBlock>Hello</TextBlock> </Border> <Border Canvas.Left=’85’ Canvas.Top=’50’ BorderThickness=’4,1,4,1’ CornerRadius=’0,15,0,15’ BorderBrush=’Black’ Padding=’5’> <TextBlock>Hello</TextBlock> </Border> </Canvas> Рис. 3.39. Всплывающее окно поверх обычного 171 172 Глава 3. Элементы управления Popup В прошлом создание плавающих окон вызывало у разработчиков определен# ные трудности. Проблема в том, как окно верхнего уровня может служить кон# тейнером для их содержимого. В WPF мы столкнулись с этой проблемой, когда пытались реализовать всплывающие подсказки, меню и выпадающую часть ком# бинированного списка. Оставаясь верной духу создания повторно используемых элементов управления, команда WPF придумала класс Popup, в котором инкап# сулировано все необходимое поведение: <!— PopupSample.xaml —> <Window ... x:Class=’EssentialWPF.PopupSample’ Text=’EssentialWPF’ > <StackPanel> <Button Click=’ToggleDisplay’>Toggle</Button> <Popup PopupAnimation=’Fade’ Placement=’Mouse’ Name=’_popup’ > <Button>Hello!</Button> </Popup> </StackPanel> </Window> // PopupSample.xaml.cs ... void ToggleDisplay(object sender, RoutedEventArgs e) { _popup.IsOpen = !_popup.IsOpen; } ... Эта программа выводит окно, показанное на рис. 3.39. ScrollViewer Всюду, где необходима прокрутка, на помощь приходит элемент ScrollViewer. Возможно, вы обратили внимание, что классы, реализующие полосы прокрутки, находятся в пространстве имен Primitives; это объясняется тем, что их следует ис# пользовать только для прокрутки, а единственный компонент, который должен реализовывать прокрутку, – это ScrollViewer. Пользоваться элементом ScrollViewer следует с осторожностью, так как в со# четании с менеджерами размещения он может давать неожиданные результаты. Самая крупная неприятность состоит в том, что для вычисления размера полосы прокрутки, ScrollViewer должен знать размеры своих дочерних элементов. Но ес# ли в качестве потомков ScrollViewer выступают менеджеры размещения беско# нечного (в направлении прокрутки) размера, то различные элементы начинают путаться. Это относится, скажем, к FlowDocumentScrollViewer и, как ни странно, к дочерним элементам ScrollViewer. Библиотека элементов управления 173 Элемент ScrollViewer можно найти в визуальных стилях большинства спис# ков и редакторов, вообще любого элемента, который по умолчанию способен отображать область, выходящую за пределы видимой поверхности. Чаще всего ScrollViewer делают корневым элементом окна. Возможно, вы подумали, что та# кая организация позволит без труда реализовать диалоговое окно, которое авто# матически получит полосы прокрутки, если его сделать слишком маленьким. Действительно, это сработает, нужно лишь в качестве размера всех дочерних элементов, отображающих содержимое, указать Auto. Рассмотрим, к примеру, диалог на рис. 3.40. Рис. 3.40. Диалоговое окно и список с полосой прокрутки В нем имеется простая сетка с тремя строками (элемент Grid будет подроб# но рассматриваться в главе 4). При уменьшении размера окна хотелось бы, 174 Глава 3. Элементы управления чтобы полосы прокрутки появлялись для окна в целом, а не только для спис# ка. Простое решение заключается в том, чтобы погрузить сетку внутрь ScrollViewer: <Window ... x:Class=’EssentialWPF.ScrollViewerTest’ Title=’ScrollViewer’ > <ScrollViewer HorizontalScrollBarVisibility=’Auto’ VerticalScrollBarVisibility=’Auto’ > <Grid> <Grid.RowDefinitions> <RowDefinition Height=’Auto’ /> <RowDefinition Height=’Auto’ /> <RowDefinition /> </Grid.RowDefinitions> <TextBox Margin=’5’ Grid.Row=’0’ Name=’_toAdd’ /> <Button Margin=’5’ Grid.Row=’1’ Click=’AddIt’>Add</Button> <ListBox Margin=’5’ Grid.Row=’2’ Name=’_list’ /> </Grid> </ScrollViewer> </Window> void AddIt(object sender, RoutedEventArgs e) { _list.Items.Add(_toAdd.Text); } Поскольку список и сам имеет полосу прокрутки, то результат получается не# ожиданным. На рис. 3.41 видно, что полоса прокрутки появляется в окне, а не в списке, как мы ожидаем. Пути решения этой проблемы не существует. ScrollViewer – очень мощный инструмент, но для его правильного применения надо понимать, как он взаимодействует с менеджерами размещения. ScrollViewer позволяет «уместить» в ограниченное пространство произ# вольное содержимое, автоматически добавляя полосы прокрутки, если места перестает хватать. Еще один элемент, помогающий решить эту проблему, – Viewbox. Viewbox Элемент Viewbox принимает одного потомка и растягивает его, применяя ге# ометрические преобразования, как показано на рис. 3.42. Свойства Stretch и StretchDirection позволяют управлять масштабированием содержимого. Библиотека элементов управления 175 Рис. 3.41. Диалоговое окно, корневым элементом которого является ScrollViewer. Обратите внимание, что в списке полосы прокрутки уже нет. Глава 3. Элементы управления 176 Рис. 3.42. Элемент Viewbox масштабирует содержимое, чтобы оно заняло все доступное пространство Чего мы достигли? В основу всех элементов управления в WPF положены три основных принци# па: композиция, повсеместное использование развитого содержимого и простая модель программирования. В этой главе мы рассмотрели фундаментальные кон# цепции, которые способствуют претворению этих принципов в жизнь: модель со# держимого и шаблоны. Не упуская их из виду, мы дали краткий обзор элементов управления, поставляемых вместе с WPF. 177 Глава 4. Размещение В главе 3 мы привели обзор библиотеки тех элементов управления на плат# форме Windows Presentation Foundation, которые применяются для построения пользовательского интерфейса приложения. В серьезных приложениях одним элементом не обойтись, поэтому нужен способ позиционирования элементов. Если элементы управления входят во все ведущие каркасы для разработки графических интерфейсов вот уже больше 15 лет, то идея управления размеще# нием появилась с запозданием. Для разработчиков на платформе Win32 разме# щение основано на использовании простой двумерной системы координат с на# чалом в левом верхнем углу окна. С другой стороны, в HTML#документах можно применять сочетание потока текста, таблиц и абсолютного позиционирования. В WPF концепция размещения глубоко интегрирована во все элементы уп# равления. Механизм размещения в WPF состоит из двух частей: контракта, описывающего, как компонент принимает участие в размещении, и набора реа# лизаций этого контракта1. Никакого встроенного размещения нет; все конкрет# ные способы размещения построены на базе механизмов расширяемости плат# формы. Принципы размещения Разработчики WPF хорошо понимали, что управление размещением должно стать неотъемлемой частью системы. Мы ставили себе целью определить единую систему размещения, которая охватывала бы все – от разбиения документов на страницы до традиционных пользовательских интерфейсов. В конце концов мы осознали, что создать единую монолитную систему, пригодную для такого широ# кого спектра сценариев, невозможно2 (или, по крайней мере, очень трудно), и ре# шили перейти к модели композиции менеджеров размещения. В результате к за# даче размещения мы подошли так же, как к созданию библиотеки элементов уп# равления, то есть разрешили вложенность менеджеров. 1 2 В этой главе не рассматривается один из важнейших компонентов размещения: текст. Тут возни) кают интересные вопросы, касающиеся разбиения на страницы, организации колонок и оптималь) ном для чтения представлении (к примеру, переносы и выравнивание). Размещение текста реали) зовано в виде расширения базовой модели. Подробно мы будем рассматривать его в главе 5. При попытке создания единого механизма возникают разнообразные проблемы. Разбиение на страницы ) лишь один из самых очевидных примеров. Если размещение ориентировано на доку) менты, то все компоненты должны иметь представление о странице (то есть о том, что нечто мо) жет пересекать границы страниц). Разбиение на страницы сильно усложняет каждый элемент уп) равления, из)за чего становится очень трудно реализовать элементы так, чтобы они корректно ве) ли себя во всех ситуациях. В результате возникает тенденция к созданию в системе замкнутого на) бора объектов, допускающих разбиение на страницы, а все остальное рассматривается как чер) ный ящик, который не может пересекать границы страниц. 178 Глава 4. Размещение При таком подходе самым важным казалось определить, как дочерний эле# мент должен взаимодействовать с родительским менеджером размещения. Мы надеялись, что «контракт» между родителем и потомком позволит размещать лю# бой элемент управления в любом менеджере. Контракт о размещении Контракт о размещении определяет способ взаимодействия между менедже# ром и дочерним элементом. Задача в том, чтобы контракт был как можно проще, но при этом позволял совместно работать различным реализациям менеджеров размещения. Хотя семантика некоторых менеджеров сложна и требует глубоких знаний о дочерних элементах, команда разработчиков WPF полагала, что у всех реализаций менеджеров должно быть нечто общее. Менеджер должен уметь оп# ределять размер каждого дочернего элемента и сообщать своему родительскому менеджеру о собственном предпочтительном размере. В итоге все сводится к двум простым идеям: адаптация к содержимому и двухэтапное размещение. Адаптация к содержимому Как следует из названия, идея адаптации менеджера к содержимому заклю# чается в том, чтобы определить собственный размер в зависимости от содержи# мого. Эта концепция применяется на всех уровнях пользовательского интерфей# са: окна могут подгонять свой размер под находящиеся в них элементы, кнопки – подстраиваться под надписи на них, а текстовые поля – изменять размер так, что# бы поместились все символы. Чтобы реализовать идею на практике, менеджер должен спросить у каждого элемента, какой размер тот предпочитает. Двухэтапное размещение Предпочтительный размер элемента управления определяется в два этапа: из# мерение и установка. Эта модель двухэтапного размещения дает и родителю, и потомку шанс достичь соглашения об окончательном размере элемента. На этапе измерения мы обходим все дерево отображения и запрашиваем у каждо# го элемента его предпочтительный размер с учетом ограничения. Элемент должен вернуть реальный размер, даже если ограничения допускают бесконечную величину (например, при размещении в контейнере с прокруткой). После того как все элемен# ты измерены, мы приступаем к этапу установки, в ходе которого родитель требует от каждого дочернего элемента, чтобы тот установил определенный размер и положение. Эта модель позволяет родителю и потомку договариваться о потребном месте на экране. Обсуждаются три представляющих интерес размера: располагаемый, предпочтительный и фактический. Располагаемый размер (available size) – это начальное ограничение, используемое на этапе измерения, как правило макси# мальная область, которую родитель готов предоставить своему потомку. Пред почтительный размер (desired size) – это размер, который желал бы установить потомок. Фактический размер (actual size) – это тот окончательный размер, ко# торый родитель назначает потомку. В идеальном мире эти величины удовлетво# ряли бы следующему соотношению: Принципы размещения 179 desiredSize <= actualSize <= availableSize В иерархии классов WPF менеджер размещения представлен базовым клас# сом UIElement. Та часть объектной модели UIElement, которая относится к ме# неджеру размещения, довольна проста. Методы Measure, MeasureCore, Arrange и ArrangeCore реализуют два этапа размещения, а свойство Visibility говорит, дол# жен ли элемент отображаться и учитываться при размещении3: public class UIElement : Visual { ... public bool Visibility Visibility { get; set; } public void Arrange(Rect finalRect); protected virtual void ArrangeCore(Rect finalRect); public void Measure(Size availableSize); protected virtual Size MeasureCore(Size availableSize); ... } Рис. 4.1. Видимая, свернутая и скрытая кнопки. Обратите внимание, что для свернутой кнопки место вообще не отводится. Свойство Visibility позволяет задать три способа участия дочернего элемента в размещении. По умолчанию оно равно Visible, то есть все элементы видимы; они отображаются и занимают место на экране. Если значение этого свойства равно Hidden, то элемент скрыт (не отображается, но занимает место), а если Collapsed, то элемент свернут (не отображается и не занимает места). На рис. 4.1 изображены три кнопки с разными значениями свойства Visibility. Пунктирная рамка показывает, какое место занимает элемент. Базовый контракт о размещении сформулирован максимально гибко, чтобы не налагать излишних ограничений на реализацию. На его основе оказалось воз# можно реализовать развитые типографические макеты и самые разные двумер# ные способы размещения4. Эта базовая модель – один из самых важных строи# тельных блоков; при решении задач о размещении на плоскости выявилось нема# ло общих паттернов. 3 4 Смысл двух наборов методов в том, чтобы отделить методы, который нужно переопределить при реализации менеджера размещения (ArrangeCore и MeasureCore), от открытых методов (Arrange и Measure). Например, при реализации панели размещения для дочерних элементов всегда должны вызываться методы Arrange и Measure; методы же ArrangeCore и MeasureCore никогда не вызываются явно. Решение о таком разделении было принято для того, чтобы сис) тема могла выполнять какие)то нетривиальные действия (скажем, кэширование или обновле) ние экрана) на этапах измерения и установки. Размещения в трехмерном пространстве — очень интересная тема, но в настоящее время ее луч) ше оставить теоретикам. 180 Глава 4. Размещение Согласованное размещение Чтобы упростить реализацию менеджеров размещения, согласованных с эти# ми паттернами, в WPF имеется слой поверх контракта о размещении. Свойства, управляющие общими паттернами размещения, определены в пространстве имен System.Windows.FrameworkElement. Пожалуй, самым общим из всех выявленных нами паттернов, является поня# тие ограничения на размещение. При построении пользовательского интерфейса часто бывает необходимо задать минимальный и максимальный размер или даже указать конкретный размер (то есть запретить адаптацию под содержимое). Ограничения на размещение Для задания ограничений на размещение в классе FrameworkElement определе# но шесть свойств: MinWidth, MinHeight, MaxWidth, MaxHeight, Width и Height: public class FrameworkElement : UIElement { public double Height { get; set; } public double MaxHeight { get; set; } public double MaxWidth { get; set; } public double MinHeight { get; set; } public double MinWidth { get; set; } public double Width { get; set; } // другие члены FrameworkElement ... } Обычно свойства Width и Height явно не задаются, поскольку иначе адапта# ция под размер содержимого была бы вообще отключена. С другой стороны, свойства MinWidth, MinHeight, MaxWidth и MaxHeight позволяют наложить ограничения на размещение, не запрещая адаптацию. Например, вместо того чтобы указывать ширину кнопки, мы можем присвоить свойству MinWidth предпочтительную ширину. Если будет задана очень длинная надпись, то кноп# ка расширится, так чтобы вместить ее целиком, но обычно будет иметь предпоч# тительный размер. Свойства ActualHeight и ActualWidth можно только читать, они позволяют узнать фактический размер элемента. Отметим, что их значения достоверны только по завершении обоих этапов размещения (измерения и установки). Выбор типа данных свойств Width и Height стал предметом горячих споров при разработке WPF. Проблема была в том, как поступать с процентами и автома# тическим выбором размера5. Когда задается автоматический выбор ширины или высоты, система размещения понимает, что размер элемента следует адаптировать под содержимое. Учитывая, что такое поведение подразумевается по умолчанию, авторы решили, что отсутствие явно заданной ширины или высоты будет означать автоматический выбор6. Но придумать, как быть с заданием размеров в процентах, 5 6 Менее серьезная проблема была связана с тем, как обращаться с физическими единицами изме) рения (дюймами, сантиметрами и т.д.). Но поскольку все линейные размеры в WPF выражаются в 1/96 дюйма, то для поддержки произвольной единицы достаточно тривиальной арифметики. Можно также использовать значение Double.NaN. Принципы размещения 181 оказалось гораздо труднее. Для некоторых элементов управления (например, Grid или Table) понятие процентной доли естественно. Другие (например, Canvas) поддержать его не могут в принципе (ниже мы увидим, почему). Модель слотов Если возможность задания размеров в процентах так сильно зависит от конк# ретного элемента управления, то необходимо, чтобы элемент мог определять свое поведение при использовании в качестве дочернего. Интересно, кстати, что най# денное решение помогло справиться и с другим общим паттерном: полями. На вычисление размеров области, отводимой родителем для дочернего эле# мента, влияют разные свойства, но не следует забывать и о пустых промежутках, разделяющих элементы. Если вам доводилось работать с CSS, то вы знакомы с ящичной моделью, в которой для каждого элемента определены поля, отступы и границы. В WPF мы остановились на более простой модели, учитывающей ком# позиционную природу системы размещения. Концептуальный смысл модели размещения в том, что родитель выделяет пространственный слот для дочернего элемента. Элемент может занять любую часть этого слота. Такая гибкость оставляет возможность для компромисса меж# ду родителем и потомком в случае возникновения конфликтов. Родитель задает границы, а потомок волен вести себя в этих границах, как ему заблагорассудится. В модели программирования эта функциональность выражена в виде трех свойств: Margin, HorizontalAlignment и VerticalAlignment. Свойство Margin (поле) позволяет потомку оставить вокруг себя пустое пространство – внутри слота, но вне содержимого. Свойства HorizontalAlignment и VerticalAlignment описывают, как потомок распорядится оставшимся в слоте пространством (выравнивание определяется относительно области, из которой поля уже исключены). Рис. 4.2. Выравнивание кнопок внутри панели Глава 4. Размещение 182 public class FrameworkElement : UIElement { public HorizontalAlignment HorizontalAlignment { get; set; } public Thickness Margin { get; set; } public VerticalAlignment VerticalAlignment { get; set; } // другие члены FrameworkElement ... } На рис. 4.2 показаны два разных объекта StackPanel. Для верхней панели задана ориентация по вертикали (кнопки располагаются друг под другом сверху вниз), а для нижней – по горизонтали (кнопки располагаются одна за другой слева напра# во). Пунктирными линиями обозначены границы слотов, которые панель отвела каждому дочернему элементу. С помощью свойств HorizontalAlignment и VerticalAlignment можно управлять тем, какую часть слота будет занимать элемент: <StackPanel Orientation=’Vertical’ Background=’...’> <Button HorizontalAlignment=’Center’>Center</Button> <Button HorizontalAlignment=’Left’>Left</Button> <Button HorizontalAlignment=’Right’>Right</Button> <Button HorizontalAlignment=’Stretch’>Stretch</Button> </StackPanel> <StackPanel Orientation=’Horizontal’ Background=’...’> <Button VerticalAlignment=’Center’>Center</Button> <Button VerticalAlignment=’Top’>Top</Button> <Button VerticalAlignment=’Bottom’>Bottom</Button> <Button VerticalAlignment=’Stretch’>Stretch</Button> </StackPanel> Свойство Margin позволяет задать поля вдоль каждой стороны слота или од# но значение для всех сторон. Как видно из рис. 4.3, поля могут быть и отрицатель# ными (пунктирными линиями снова обозначены границы слотов): Рис. 4.3. Задание полей для кнопок <StackPanel Orientation=’Vertical’ Background=’...’> <Button Margin=’0’ HorizontalAlignment=’Stretch’>No margin</Button> <Button Margin=’5’ HorizontalAlignment=’Stretch’>5 all around</Button> <Button Margin=’10,0,10,0’ HorizontalAlignment=’Stretch’> 10 on left and right</Button> Принципы размещения 183 <Button Margin=’15,0,0,0’ HorizontalAlignment=’Stretch’> Negative on the left</Button> </StackPanel> На рис. 4.3 показана еще одна не такая заметная, но важная вещь: по умолчанию от# сечение не производится. Что это означает? Обратите внимание, что последняя кноп# ка выступает за границы родительской панели. В Win32 в этом случае выступающие части дочерних элементов по умолчанию отсекались; лишь в очень редких случаях по# томку разрешалось выйти за пределы родительского контейнера. А еще реже удава# лось получить корректный результат, если потомок рисовал за пределами родителя. В главе 1 уже отмечалось, что в WPF используется механизм композиции, при# чем элементы рисуются, начиная с самого отдаленного от зрителя (алгоритм худож# ника). Отсечение – это дорогостоящая операция: при рисовании каждого пикселя надо проверить, попадает ли он внутрь области отсечения. Достоинства отсечения проявляются при рисовании отдельно взятого элемента управления. В мире, где от# сечения нет, мы никогда не узнаем, что какой#то элемент пожелал что#то нарисовать за пределами отведенной ему области и тем самым испортил нам всю картину. WPF готова мириться с падением производительности из#за повсеместной ком# позиции, предоставляя в качестве компенсации гораздо более развитые средства ви# зуализации. К тому же, современные видеокарты и процессоры позволяют снизить издержки. Но в мире, где всем правит композиция, отсечение обходится еще дороже. Большинство элементов управления в WPF пользуются механизмом размеще# ния, который гарантирует попадание внутрь родительского контейнера, поэтому не# обходимости в отсечении не возникает. Но некоторые элементы (например, Button) все же включают отсечение, чтобы содержимое не «вытекало» за пределы элемента. Это обсуждение подводит нас еще к одному интересному паттерну размеще# ния: преобразованиям. Отрицательные поля позволяют вынести элемент на пре# делы той области, которую ему отвел родитель. Преобразования позволяют доби# ваться подобных результатов куда более гибким способом. Рис. 4.4. Три кнопки без применения преобразований Преобразования Когда все прочие средства исчерпаны, остается кувалда. Для установки оконча# тельного положения элемента существуют два свойства, позволяющие выполнить произвольное геометрическое преобразование: RenderTransform и LayoutTransform. public class FrameworkElement : UIElement { public Transform LayoutTransform { get; set; } public Transform RenderTransform { get; set; } // другие члены FrameworkElement ... } 184 Глава 4. Размещение Оба свойства имеют тип System.Windows.Media.Transform, который мы рас# смотрим в главе 5. А пока можно изучить его на примере наиболее употребитель# ных подклассов: ScaleTransform, RotateTransform, TranslateTransform и TransformGroup, Класс ScaleTransform позволяет выполнить растяжение или сжатие по оси x/y, RotateTransform – осуществлять поворот вокруг заданной точ# ки, а TranslateTransform – производить параллельный перенос на заданный век# тор. Наконец, класс TransformGroup служит для произвольного комбинирования трех примитивных преобразований. Единственное различие между RenderTransform и LayoutTransform заключает# ся в том, когда применяется преобразование. Преобразования, хранящиеся в свой# стве RenderTransform, применяются непосредственно перед выводом на экран7 (и, значит, затрагивают только вывод), а те, что хранятся в LayoutTransform, – перед размещением (и, следовательно, сказываются и на размещении компонента). Чтобы понять, как эти два свойства взаимодействуют, начнем с простой панели StackPanel, содержащей три кнопки (рис. 4.4): <StackPanel Background=’...’ Orientation=’Horizontal’> <Button Width=’75’> 15 </Button> <Button Width=’75’> 45 </Button> <Button Width=’75’> 65 </Button> </StackPanel> Чтобы повернуть каждую кнопку на определенный угол, можно воспользо# ваться свойством RenderTransform, LayoutTransform или обоими. Если добавить преобразование RotateTransform в свойство RenderTransform для каждой кноп# ки, получится результат, изображенный на рис. 4.5: <StackPanel Background=’...’ Orientation=’Horizontal’> <Button Width=’75’> <Button.RenderTransform> <RotateTransform Angle=’15’ /> </Button.RenderTransform> 15 </Button> <Button Width=’75’> <Button.RenderTransform> <RotateTransform Angle=’45’ /> 7 Употреблять слова «перед» и «после» применительно к выводу на экран не вполне корректно. Поскольку WPF — система с сохранением изображения, то имеет смысл говорить о моменте до или после генерирования списка команд рисования. Свойство RenderTransform заносит преобразование в список команд, но ничего не сообщает о нем системе размещения. Свой) ство LayoutTransform тоже помещает преобразование в список команд, но еще и применяет его на этапе измерения данного элемента. Принципы размещения 185 </Button.RenderTransform> 45 </Button> <Button Width=’75’> <Button.RenderTransform> <RotateTransform Angle=’65’ /> </Button.RenderTransform> 65 </Button> </StackPanel> Отметим, что на рис. 4.5 кнопки перекрываются и выступают за границы па# нели. Поскольку преобразование применено после размещения, система разме# щения ничего не знает о манипуляциях, произведенных над образом каждой кнопкой. По сути дела, элементы управления солгали, сказав, что они занимают позиции до преобразования. Если воспользоваться свойством LayoutTransform, результат будет совершенно иным (рис. 4.6): Рис. 4.5. Задание свойства RenderTransform для каждой кнопки Рис. 4.6. Задание свойства LayoutTransform для каждой кнопки <StackPanel Background=’...’ Orientation=’Horizontal’> <Button Width=’75’> <Button.LayoutTransform> <RotateTransform Angle=’15’ /> </Button.LayoutTransform> 15 </Button> <Button Width=’75’> <Button.LayoutTransform> <RotateTransform Angle=’45’ /> </Button.LayoutTransform> 45 </Button> Глава 4. Размещение 186 <Button Width=’75’> <Button.LayoutTransform> <RotateTransform Angle=’65’ /> </Button.LayoutTransform> 65 </Button> </StackPanel> Теперь кнопки не перекрываются, а размер панели изменился так, что они по# мещаются в ней целиком. Поскольку хранящиеся в свойстве LayoutTransform пре# образования применяются до размещения, то система знает о том, к чему они при# вели. В общем случае, для получения визуальных эффектов (например, анимации при входе) лучше пользоваться свойством RenderTransform. Так как преобразова# ния, применяемые при выводе на экран, не затрагивают размещения, их можно выполнять целиком в видеокарте, избежав вычислений при создании эффекта. Z)индекс Для перекрывающихся элементов управления определено понятие упорядо# чения вдоль оси z – z#индекс. По умолчанию у всех элементов управления z#ин# декс равен 0. Перекрытие определяется взаимным положением потомков в набо# ре Panel.Children. С помощью свойства Panel.ZIndex мы можем организовать «уровни» потомков. Внутри одного уровня порядок вывода потомков на экран определяется тем, какой раньше помещен в набор; а для точного управления пе# рекрытием можно задать несколько уровней. Для иллюстрации мы можем создать несколько кнопок, задав для каждой от# рицательное поле, чтобы все они перекрывались: <WrapPanel> <Button Margin=’ <Button Margin=’ <Button Margin=’ <Button Margin=’ </WrapPanel> 5’>Button 5’>Button 5’>Button 5’>Button One</Button> Two</Button> Three</Button> Four</Button> Эта программа формирует картинку, изображенную на рис. 4.7 сверху. Пер# вый элемент находится в самом низу визуальной стопки. Задав z#индекс для нес# кольких кнопок, мы сможем изменить картину: <WrapPanel> <Button Panel.ZIndex=’2’ Margin=’ 5’>Button One</Button> <Button Margin=’ 5’>Button Two</Button> <Button Panel.ZIndex=’1’ Margin=’ 5’>Button Three</Button> <Button Margin=’ 5’>Button Four</Button> </WrapPanel> Отметим, что у нескольких элементов значение z#индекса одинаково (в дан# ном случае для второй и четвертой кнопки z#индекс равен 0). Результат показан на рис. 4.7 снизу. Принципы размещения 187 z-индекс не задан z-индекс задан Рис. 4.7. Влияние z)индекса на размещение элементов управления Реализация согласованного размещения Мы познакомились со многими свойствами размещения, определенными в классе FrameworkElement. С их помощью можно визуально изменить размещение, не занима# ясь программированием новой панели. Но если все#таки приходится реализовывать но# вую панель, то задача может показаться пугающе сложной, если учесть все рассмотрен# ные выше варианты поведения. К счастью, поведение целиком скрыто внутри: public class FrameworkElement : UIElement { public void Arrange(Rect finalRect); protected override sealed void ArrangeCore(Rect finalRect); protected virtual Size ArrangeOverride(Size finalSize); public void Measure(Size availableSize); protected override sealed Size MeasureCore(Size availableSize); protected virtual Size MeasureOverride(Size availableSize); } Класс FrameworkElement переопределяет методы ArrangeCore и MeasureCore и вводит новые: ArrangeOverride и MeasureOverride. Чтобы реализовать новый менеджер размещения, поддерживающий все описанные выше паттерны, необхо# димо переопределить методы ArrangeOverride и MeasureOverride8. Прочие свой# ства размещения можно игнорировать. Отсутствие встроенного размещения Возможно, вы подумали, будто все свойства размещения встроены в базовые типы. Памятуя о разнообразных свойствах для задания размеров, выравнивания, полей и преобразований, легко прийти к такому выводу. Напомним, однако, что большинство 8 Вспомните разговор о парах Measure/Arrange и MeasureCore/ArrangeCore и о том, как косвенный вызов последних позволяет системе выполнять какие)то функции на этапах измерения и установ) ки. В класс FrameworkElement мы добавили множество таких функций (модель слотов и т.д.) Пере) определив Core)методы и предоставив новые точки входа, мы дали разработчикам возможность переопределять только методы MeasureOverride и ArrangeOverride, не заботясь об остальных дета) лях механизма размещения. 188 Глава 4. Размещение этих свойств определено в классе FrameworkElement, а не UIElement. Мы хотели провести четкую грань между контрактом о размещении – этапах измерения и уста# новки – и реализацией конкретных менеджеров размещения. На ранних стадиях разработки WPF возникали горячие споры о том, как реа# лизовывать менеджеры размещения. Мы считали это критически важным для создания системы с развитыми механизмами размещения. Хотелось получить ос# нованную на композиции систему, которая допускала бы расширения. Поэтому мы решили не «зашивать» знание о каждом менеджере в каждый элемент. Самый удачный пример достаточно развитой, расширяемой и основанной на композиции системы размещения – это, пожалуй, Windows Forms. В версии 1.0 была реализована простая модель размещения на базе события Layout. У каждо# го элемента управления было два свойства – Dock и Anchor, которые использо# вались для выполнения стандартного размещения. Но чтобы реализовать специ# ализированное размещение, нужно было изобрести какой#то иной механизм (быть может, на основе IExtenderProvider?), чтобы у элементов появились новые свойства. В Windows Forms 2.0 система размещения стала более расширяемой, улучшилась поддержка для добавления свойств в элементы управления. Впро# чем, свойства Dock и Anchor по#прежнему остались. Чтобы не увеличивать количество свойств элементов до бесконечности, все свойства, имеющие отношение к реализации размещения, сделаны присоединен# ными (например, Canvas.Left, которое мы обсудим ниже). Присоединенные свой ства – это механизм, с помощью которого один объект может наделять неким свойством другие объекты. Поддержка присоединенных свойств реализована с помощью класса DependencyObject, которому наследуют почти все типы в WPF. Получив представление о принципах размещения, мы можем ознакомиться с библиотекой менеджеров размещения WPF. Библиотека менеджеров размещения На основе базового элемента управления и более полной модели размещения в WPF реализован целый набор различных панелей. Они обеспечивают типич# ные способы размещения, которых должно быть достаточно для наиболее часто встречающихся случаев. Мы рассмотрим наиболее употребительные панели: Canvas, StackPanel, DockPanel и UniformGrid. А самой сложной – Grid – посвя# тим отдельный раздел. Панель Canvas Canvas – простейший из всех менеджеров размещения в WPF. У него есть че# тыре свойства: Top, Left, Right и Bottom. Этот элемент позволяет расположить своих потомков с любым смещением от одного угла панели. Именно «располо# жить», поскольку элемент Canvas не налагает на размеры потомков никаких огра# ничений. Он просто запрашивает у дочернего элемента предпочтительный размер и позиционирует его относительно одного из четырех углов. Разрешается однов# ременно задавать только два свойства: одну горизонтальную и одну вертикальную координату. При попытке задать больше свойств лишние просто игнорируются. Библиотека менеджеров размещения 189 Рис. 4.8. Варианты размещения кнопок на панели Canvas Для иллюстрации поместим на панель Canvas несколько кнопок с разными смещениями. На рис. 4.8 видно, что смещается соответствующий угол дочернего элемента; например, если задать свойства Canvas.Right и Canvas.Top, то будет смещен правый верхний потомка: <Canvas Width=’200’ Height=’100’ Background=’...’ > <Button Canvas.Right=’2’ Canvas.Top=’2’> Top, Right </Button> <Button Canvas.Left=’2’ Canvas.Top=’2’> Top, Left </Button> <Button Canvas.Right=’2’ Canvas.Bottom=’2’> Bottom, Right </Button> <Button Canvas.Left=’2’ Canvas.Bottom=’2’> Bottom, Left </Button> <Button Canvas.Left=’20’ Canvas.Bottom=’20’ Canvas.Right=’20’ Canvas.Top=’20’ > All Four </Button> </Canvas> Canvas не налагает каких#либо интересных ограничений на ширину или высоту сло# та, поэтому свойства HorizontalAlignment и VerticalAlignment несущественны. Поля тем не менее учитываются, но с точки зрения поведения они ничем не отличаются от свойств самой панели. Поэтому элемент Canvas обычно называют «бесслотовой» панелью. Рис. 4.9. Чудесным образом отсутствующая панель Canvas Глава 4. Размещение 190 У элемента Canvas есть еще одна интересная особенность. При обсуждении свойства RenderTransform мы говорили, что по умолчанию отсечение для потом# ков не производится. Поскольку потомки могут быть ориентированы относи# тельно любого из четырех углов, Canvas не может определить свою предпочти# тельную ширину и высоту. Это означает, что легко создать ситуацию, когда сама панель не имеет размеров, а ее потомки все равно видимы. На первый взгляд, по# ведение странное, но таким способом можно размещать элементы управления, плавающие поверх остальных частей пользовательского интерфейса. Это быва# ет удобно. Чтобы убедиться в этом, поместим Canvas внутрь StackPanel и уберем свой# ства Width и Height (задание выравнивания по горизонтали и вертикали гаран# тирует, что размер Canvas будет адаптирован к содержимому по обоим направ# лениям): <StackPanel> <Canvas HorizontalAlignment=’Center’ VerticalAlignment=’Center’ Background=’...’ > <Button ...>Top, Right</Button> <Button ...>Top, Left</Button> <Button ...>Bottom, Right</Button> <Button ...>Bottom, Left</Button> <Button ...>All Four</Button> </Canvas> </StackPanel> Запустив эту программу (рис. 4.9), мы не увидим панель Canvas, а кнопки все равно будут распределены по углам. Рис. 4.10. Применение Canvas для плавающего размещения элементов Здесь Canvas на самом деле имеет нулевую высоту, а по ширине StackPanel растягивает ее на весь экран. Чтобы понять, как можно воспользоваться этой осо# бенностью на практике, добавим еще несколько кнопок в стопку: <StackPanel> <Button>Button 1</Button> <Button>Button 2</Button> <Canvas HorizontalAlignment=’Center’ VerticalAlignment=’Center’ > <Button Canvas.Right=’4’ Canvas.Bottom=’18’>from Библиотека менеджеров размещения 191 Canvas</Button> </Canvas> </StackPanel> Как показывает рис. 4.10, кнопка, находящаяся внутри Canvas, «плавает» по# верх остальных. Пунктирными линиями представлены слоты в StackPanel; высо# та слота для панели Canvas нулевая. Этот трюк позволяет вставлять элементы в панель, не затрагивая размещения других компонентов. Панель StackPanel До сих пор мы только с панелью StackPanel и сталкивались, так что вы, навер# ное, уже разобрались с тем, как она работает, но еще несколько слов не повредят. Менеджер StackPanel размещает своих потомков построчно. Свойство Orientation позволяет задать ориентацию стопки: горизонтальную или верти# кальную. Каждому дочернему элементу StackPanel выделяет слот, совпадающий с ним по ширине или высоте (в зависимости от ориентации), а свой предпочтительный размер вычисляет на основе максимальных размеров своих потомков. Чтобы убе# диться в этом, организуем вложенные панели StackPanel и посмотрим, как они будут себя вести. Внешний экземпляр имеет рамку (чтобы его было видно), а для каждого внутреннего задан цвет фона: <Border BorderBrush=’Black’ BorderThickness=’2’ HorizontalAlignment=’Center’ VerticalAlignment=’Center’> <StackPanel Orientation=’Vertical’> <StackPanel Margin=’5’ Background=’...’ Orientation=’Horizontal’> <Button Margin=’2’>One</Button> <Button Margin=’2’>Two</Button> <Button Margin=’2’>Three</Button> </StackPanel> <StackPanel Margin=’5’ Background=’...’ Orientation=’Vertical’> <Button Margin=’2’>One</Button> <Button Margin=’2’>Two</Button> <Button Margin=’2’>Three</Button> </StackPanel> </StackPanel> </Border> На рис. 4.11 видно, что горизонтальная панель размещает кнопки друг за дру# гом. Кнопки растягиваются так, чтобы их высота была одинаковой. Вертикальная панель размещает кнопки одну под другой, и все они имеют одинаковую ширину (поскольку явно мы никакое выравнивание не задавали). Такими широкими кнопки оказались потому, что внешняя панель принудительно уравнивает шири# ну своих дочерних панелей. Если говорить о сложности (и мощи) композиции в механизме размещения, то это еще цветочки. Пользоваться панелью StackPanel следует с осторожностью, так как она из# меряет своих потомков в предположении бесконечности собственной ширины Глава 4. Размещение 192 или высоты (в зависимости от ориентации). Отсутствие контроля над разме# ром может оказать негативное воздействие на дочерние панели, особенно это относится к элементу TextBlock с включенным переносом по словам (рис. 4.12) и к ScrollViewer: <StackPanel Background=’...’ Width=’85’ Orientation=’Horizontal’> <TextBlock FontSize=’18pt’ TextWrapping=’Wrap’> this is a test of stack panel wrapping </TextBlock> </StackPanel> Решить эту проблему при использовании панели StackPanel невозможно. Са# мый распространенный обходной путь заключается в применении вместо нее DockPanel. Панель DockPanel Менеджер DockPanel во многом похож на StackPanel, но допускает размеще# ние вдоль разных сторон. Кроме того, свойство LastChildFill позволяет последне# му потомку занять все свободное пространство (как в случае DockStyle.Fill в Windows Forms). Рис. 4.11. Вложенные панели StackPanel Рис. 4.12. Горизонтальная панель StackPanel отсекает, а не переносит текст Панель DockPanel, наверное, самая употребительная из всех применяемых в современных графических интерфейсах. Windows Forms поддерживает стыковку на уровне базовых средств, а Java – с помощью класса BorderLayout. Механизм стыковки позволяет размещать элементы вдоль любой стороны контейнера, при# чем последний элемент занимает все оставшееся место. Такой способ применяет# ся, в частности, в программе Windows Explorer (рис. 4.13). В окне Windows Explorer можно выделить следующие основные структурные эле# менты: меню, панель инструментов, список папок и панель детализации (рис. 4.14). Библиотека менеджеров размещения 193 Реализовать такое размещение в WPF относительно просто. Панель DockPanel предлагает единственное свойство Dock, которое позволяет указать, к какой сто# роне нужно пристыковать элемент. Порядок объявления дочерних элементов оп# ределяет и порядок их размещения, последний элемент по умолчанию заполняет все оставшееся место. Рис. 4.13. Windows Explorer – классический пример размещения со стыковкой Меню Панель инструментов Список папок Панель детализации Рис. 4.14. Windows Explorer – четыре основных части окна 194 Глава 4. Размещение Воспроизвести структуру Windows Explorer можно следующим образом (рис. 4.15); для моделирования основных элементов мы взяли кнопки: <DockPanel> <Button DockPanel.Dock=’Top’>Menu Area (Dock=Top)</Button> <Button DockPanel.Dock=’Top’>Toolbar Area (Dock=Top)</Button> <Button DockPanel.Dock=’Left’>Folders (Dock=Left)</Button> <Button>Content (Fills remaining space because LastChildFill=’true’)</Button> </DockPanel> Рис. 4.15. Использование панели DockPanel для построения классического размещения со стыковкой Можно изменить взаимное расположение частей (например, если мы хотим, чтобы список папок занимал по вертикали все место от меню до нижней границы окна). Для этого достаточно переупорядочить элементы: <DockPanel> <Button DockPanel.Dock=’Top’>Menu Area (Dock=Top)</Button> <Button DockPanel.Dock=’Left’>Folders (Dock=Left)</Button> <Button DockPanel.Dock=’Top’>Toolbar Area (Dock=Top)</Button> <Button>Content (Fills remaining space because LastChildFill=’true’)</Button> </DockPanel> Размеры следующего слота вычисляются после того, как DockPanel выделит преды# дущий, поэтому никакие два слота не перекрываются. Поскольку список папок идет сра# зу после меню, то и слот для него выделяется вслед за слотом для меню (рис. 4.16). Библиотека менеджеров размещения 195 Отметим, что по умолчанию DockPanel не позволяет пользователю изменять размещение (то есть использовать подвижный разделитель между списком папок и панелью детализации для изменения размера списка). В WPF есть только один разделитель – GridSplitter, тесно связанный с элементом Grid, который мы рас# смотрим чуть ниже. Рис. 4.16. Изменение порядка дочерних элементов отражается на размещении Рис. 4.17. Панель WrapPanel в действии Панель WrapPanel Если DockPanel ведет себя как стопка с несколькими сторонами, то WrapPanel – это стопка с поддержкой переноса на новую строку. Напомним, что StackPanel размещает потомков, не ограничивая суммарную ширину или высоту (в зависи# мости от ориентации), то есть последовательно может располагаться произволь# ное число элементов. С другой стороны, WrapPanel упаковывает элементы в дос# тупном пространстве, а когда оно исчерпывается, производит перенос на следую# щую строку. Классический пример такой стратегии – панель инструментов. Глава 4. Размещение 196 По умолчанию WrapPanel устанавливает размеры своих потомков, адаптиру# ясь к их содержимому (см. рис. 4.17), хотя мы можем задать размеры и явно с по# мощью свойств ItemWidth и ItemHeight: <WrapPanel Background=’...’> <Button>One</Button> <Button>Two</Button> <Button>Three</Button> <Button>Four</Button> <Button>Five</Button> <Button>Six</Button> </WrapPanel> Панель UniformGrid И последний из базовых менеджеров размещения, который не имеет ничего общего со StackPanel, – это UniformGrid. Этот класс, определенный в простран# стве имен System.Windows.Controls.Primitives, реализует очень простое размеще# ние в сетке. Все ячейки имеют одинаковый размер (отсюда и слово uniform – рав# номерная), а положение дочернего элемента определяется его порядком в наборе потомков. При работе с UniformGrid мы задаем нужное число строк и столбцов. Если за# дать только число столбцов, то число строк будет вычисляться делением числа потомков на число столбцов, и наоборот: <UniformGrid Columns=’2’ Rows=’3’ Background=’...’> <Button>One</Button> <Button>Two</Button> <Button>Three</Button> <Button>Four</Button> <Button>Five</Button> <Button>Six</Button> </UniformGrid> Рис. 4.18. Панель UniformGrid размером 2x3 Эта программа выводит сетку, показанную на рис. 4.18. Ширина каждой ячейки равна ширине самого широкого потомка, а высота – высоте самого высокого потомка. Если потомков больше, чем ячеек в сетке (нап# ример, если добавить седьмого потомка в предыдущем примере), то он будет раз# мещен так, как если бы сетка имела бесконечную высоту (то есть седьмая кнопка окажется под пятой): Панель Grid 197 <UniformGrid Columns=’2’ Rows=’3’ Background=’...’> <Button>One</Button> <Button>Two</Button> <Button>Three</Button> <Button>Four</Button> <Button>Five</Button> <Button>Six</Button> <Button>Seven</Button> </UniformGrid> Результат работы этой программы показан на рис. 4.19. Темная рамка показы# вает границы сетки. Как бы странно это ни выглядело, но UniformGrid размеща# ет последнего потомка за пределами панели. На элементе UniformGrid мы завершаем краткий обзор базовых менеджеров размещения в WPF. Следующим на очереди самый сложный, но и самый мощ# ный менеджер: Grid. Рис. 4.19. Панель UniformGrid, у которой слишком много потомков. Панель Grid Панель UniformGrid – это очень простая сетка, которой в большинстве случаев недостаточно. Необходима такая сетка, в которой элементы могут занимать нес# колько соседних клеток, со строками разной высоты и столбцами разной ширины и т.д. Элемент Grid – самый гибкий, мощный и сложный из всех менеджеров раз# мещения. На первый взгляд, этот элемент кажется простым: элементы всего лишь размещаются в сетке, определяемой числом строк и столбцов. Не сложно, правда? Самый примитивный способ использования Grid состоит в том, чтобы задать свойства RowDefinitions и ColumnDefinitions, добавить несколько дочерних эле# ментов и с помощью присоединенных свойств Grid.Row и Grid.Column указать, какого потомка в какой слот поместить (рис. 4.20): <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> Глава 4. Размещение 198 <Button <Button <Button <Button </Grid> Grid.Row=’0’ Grid.Row=’0’ Grid.Row=’1’ Grid.Row=’1’ Grid.Column=’0’ Grid.Column=’1’ Grid.Column=’0’ Grid.Column=’1’ >One</Button> >Two</Button> >Three</Button> >Four</Button> Концептуальная модель элемента Grid На рис. 4.20 показано базовое размещение в сетке, но он не отражает три важ# нейших концепции: (1) отделение размещения от структуры, (2) гибкость моде# ли вычисления размеров, (3) обобществление информации о размерах. Рис. 4.20. Простой пример использования панели Grid Отделение размещения от структуры Во всех панелях, которые мы рассматривали выше, для изменения разме# щения необходимо изменить структуру дерева элементов. Сначала необходи# мо вставить панель в дерево отображения, чтобы подключить алгоритм разме# щения, а затем (иногда) еще и поменять порядок дочерних элементов, как мы видели на примере DockPanel. Вставлять панель в качестве родителя размещаемых элементов необходимо и в случае Grid, но гибкость Grid обычно позволяет обойтись гораздо меньшим ко# личеством вложенных панелей для достижения нетривиального результата. Поскольку информация о номере строки и столбца в сетке задается с по# мощью свойств, можно существенно модифицировать размещение, не меняя порядка дочерних элементов. Стало быть, порядок не влияет на размещение, поэтому оказывается намного проще управлять перекрытием и прогнозиро# вать, что получится в результате. Таким образом, мы получаем важное преи# мущество: возможность определять размещение, не затрагивая модель прог# раммирования. В случае DockPanel и StackPanel дизайнер, пожелавший изме# нить внешний вид окна или страницы, должен был бы изменить также и ие# рархию элементов, но при этом нарушилась бы работа программы. Менеджер Grid позволяет с гораздо меньшими усилиями модифицировать внешний вид, не меняя код. И, наконец, поскольку структура отделена от размещения, мы можем сначала определить состав элементов управления (скажем, три кнопки и текстовое поле), а потом применить к ним размещение (к примеру, друг за другом). Панель Grid 199 Гибкая модель вычисления размеров В большинстве панелей разбиение пространства на слоты происходит на ос# нове размера содержимого либо абсолютного значения размера дочернего эле# мента. В дополнение к этим способам, Grid вводит еще и понятие размера, выра# женного в процентах, когда ширина/высота столбца/строки задается в единицах измерения «звездочка» (*). Так описанные столбцы или строки делят между со# бой в процентном отношении пространство, оставшееся после размещения столбцов и строк, рассчитанных на базе содержимого или абсолютных размеров. Проще всего разобраться в этом на примере. Меняя свойства Width и Height в приведенной ниже разметке, мы можем влиять на размеры слотов: <Grid> <Grid.RowDefinitions> <RowDefinition Height=’50’ /> <RowDefinition Height=’1*’ /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width=’50’ /> <ColumnDefinition Width=’1*’ /> </Grid.ColumnDefinitions> <Button Grid.Row=’0’ Grid.Column=’0’ <Button Grid.Row=’0’ Grid.Column=’1’ <Button Grid.Row=’1’ Grid.Column=’0’ <Button Grid.Row=’1’ Grid.Column=’1’ </Grid> MinWidth=’0’>One</Button> MinWidth=’0’>Two</Button> MinWidth=’0’>Three</Button> MinWidth=’0’>Four</Button> Как видно из рис. 4.21, строка и столбец, для которых размеры содержат звез# дочку, занимают все место, оставшееся после размещения строки и столбца фик# сированного размера. Чтобы по#настоящему осознать всю ту гибкость, которую дает единица измере# ния «*», выразим в этих единицах размеры всех слотов. Для первой строки и пер# вого столбца укажем размер «2*». Поскольку размеры не фиксированы, то первой строке (столбцу) отводится 66.66…, а второй – 33.33… процента доступного места: <Grid> <Grid.RowDefinitions> <RowDefinition Height=’2*’ /> <RowDefinition Height=’1*’ /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width=’2*’ /> <ColumnDefinition Width=’1*’ /> </Grid.ColumnDefinitions> <Button Grid.Row=’0’ Grid.Column=’0’ <Button Grid.Row=’0’ Grid.Column=’1’ <Button Grid.Row=’1’ Grid.Column=’0’ <Button Grid.Row=’1’ Grid.Column=’1’ </Grid> MinWidth=’0’>One</Button> MinWidth=’0’>Two</Button> MinWidth=’0’>Three</Button> MinWidth=’0’>Four</Button> Глава 4. Размещение 200 Из этого примера становится понятным интересная особенность звездочки: эта единица измерения представляет взвешенную процентную долю (рис. 4.22). Если бы мы задали «20*» и «10*», результат был бы точно таким же. Помимо за# дания размера в процентах, можно указывать его еще в абсолютных единицах (пикселях) или оставить значение Auto. Последнее позволяет элементу адапти# ровать свой размер к содержимому и воздействовать на высоту строки или шири# ну колонки. Изменить размер Рис. 4.21. Единица измерения «*» означает процентную долю Автоматическое определение размера порождает интересную проблему: имея строку или столбец, размер которой вычисляется на основе размера потомков, часто хочется, чтобы другая строка (колонка) имела точно такой же размер. Изменить размер Рис. 4.22. Процентные доли, вычисляемые при задании звездочки, взвешены Панель Grid 201 Обобществление информации о размере Общий размер – это последнее из значимых средств, встроенных в менеджер Grid. Сетка делает информацию о размере доступной всем элементам управле# ния, расположенным в одном столбце. В простейшем случае это означает, что все такие элементы будут иметь одинаковый размер, определяемый самым широким из них. Для иллюстрации модифицируем пример, так чтобы надпись на одной кнопке была заметно длиннее, чем на остальных, и поместим все кнопки в один столбец (рис. 4.23): Рис. 4.23. Менеджер Grid обобществляет информацию о размере в пределах одного столбца <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width=’Auto’ /> </Grid.ColumnDefinitions> <Button Grid.Row=’0’>One</Button> <Button Grid.Row=’1’>Two</Button> <Button Grid.Row=’2’>Three (which is longer)</Button> <Button Grid.Row=’3’>Four</Button> </Grid> Если установить ширину столбца Auto, то она будет адаптироваться к со# держимому. Поскольку любая кнопка по умолчанию растягивается на всю ширину столбца (HorizontalAlignment.Stretch), то свободного места не оста# ется, а ширина столбца определяется шириной самой широкой кнопки. Та# кой вид обобществления размера очень полезен для макетирования диалого# вых окон, содержащих несколько кнопок или меток, которые желательно вы# ровнять. Глава 4. Размещение 202 Другой вид обобществления – явное задание размеров групп. Можно сказать, что две строки или столбца (даже в разных сетках) должны иметь одинаковый размер. На примере показанной выше сетки из двух строк и двух столбцов мож# но видеть, что дает подобное обобществление размеров (рис. 4.24): Без обобществления размера С обобществлением размера Рис. 4.24. Сетка с обобществленением размеров и без него Для обобществления размеров нужно сделать две вещи: во#первых, устано# вить свойство SharedSizeGroup для одной или нескольких строк (столбцов), а, во#вторых, – свойство IsSharedSizeScope для элемента управления. В первом примере мы обобществим информацию о размере двух столбцов в одной и той же сетке: <Grid IsSharedSizeScope=’true’> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width=’Auto’ SharedSizeGroup=’a’ /> <ColumnDefinition Width=’Auto’ SharedSizeGroup=’a’ /> </Grid.ColumnDefinitions> <Button Grid.Row=’0’ Grid.Column=’0’>One</Button> <Button Grid.Row=’0’ Grid.Column=’1’>Two</Button> <Button Grid.Row=’1’ Grid.Column=’0’>Three (which is longer)</Button> <Button Grid.Row=’1’ Grid.Column=’1’>Four</Button> </Grid> А на десерт сделаем то же самое для нескольких сеток. Свойство IsSharedSizeScope можно задать локально для элемента Grid или как присоеди# ненное свойство для любого элемента. Чтобы обобществить информацию о раз# мере, сделаем контекстом родительскую панель StackPanel, а затем поместим в одну группу два элемента Grid с одинаковым именем: <StackPanel Orientation=’Vertical’ Grid.IsSharedSizeScope=’true’> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width=’Auto’ SharedSizeGroup=’a’ /> Панель Grid 203 <ColumnDefinition Width=’Auto’ /> </Grid.ColumnDefinitions> <Button Grid.Row=’0’ Grid.Column=’0’>One</Button> <Button Grid.Row=’0’ Grid.Column=’1’>Two</Button> <Button Grid.Row=’1’ Grid.Column=’0’>Three (which is longer) </Button> <Button Grid.Row=’1’ Grid.Column=’1’>Four</Button> </Grid> <Border ...> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width=’Auto’ SharedSizeGroup=’a’ /> <ColumnDefinition Width=’Auto’ /> </Grid.ColumnDefinitions> <Button Grid.Row=’0’ Grid.Column=’0’>a</Button> <Button Grid.Row=’0’ Grid.Column=’1’>b</Button> <Button Grid.Row=’1’ Grid.Column=’0’>c</Button> <Button Grid.Row=’1’ Grid.Column=’1’>d</Button> </Grid> </Border> </StackPanel> Запустив эту программу, мы увидим (рис. 4.25), что первый столбец во второй сетке (с кнопками a – d и жирной черной рамкой) имеет такую же ширину, как в первой. Эта идея применяется, например, в элементе ListView, чтобы заголовок столбца имел такой же размер, как у строк в разделе данных. Организация размещения в элементе Grid Размещение в сетке состоит из двух этапов: (1) определение строк и столбцов и (2) распределение дочерних элементов по слотам. Для описания строк и столбцов применяются объекты RowDefinition и ColumnDefinition, которые частично поддерживают стандартные свойства менед# жера размещения: Рис. 4.25. Два элемента Grid с общей информацией о размерах Глава 4. Размещение 204 public class ColumnDefinition : DefinitionBase { public string SharedSizeGroup { get; set; } public double MinWidth { get; set; } public double MaxWidth { get; set; } public GridLength Width { get; set; } ... } public class RowDefinition : DefinitionBase { public string SharedSizeGroup { get; set; } public double MinHeight { get; set; } public double MaxHeight { get; set; } public GridLength Height { get; set; } ... } Эти свойства ведут себя почти так же, как одноименные свойства класса FrameworkElement, с тем отличием, что тип GridLength, которому принадлежат свойства Width и Height, позволяет задавать дополнительные единицы измере# ния, поддерживаемые элементом Grid. С помощью свойств MinWidth, MaxWidth, MinHeight и MaxHeight можно ограничить размер столбца, не уста# навливая размеров каждого элемента внутри сетки. Прежде чем приступать к определению строк и столбцов, полезно прикинуть, чего же мы хотим добиться. Предположим, наша цель – клонировать часть поль# зовательского интерфейса программы MSN Messenger (рис. 4.26). Для начала создадим эскиз. Нарисуем несколько прямоугольных областей и пунктирными линиями обозначим границы строк и столбцов (рис. 4.27). Нам понадобится две строки и два столбца. Первая строка будет занимать львиную долю всей сетки, однако мы знаем, что текстовая область может расти, чтобы было где печатать. Чтобы обеспечить такую гибкость, мы позволим второй строке адаптировать# ся к содержимому, но наложим ограничения, чтобы она не заняла все имеющееся пространство: <Grid.RowDefinitions> <RowDefinition Height=’*’ /> <RowDefinition Height=’Auto’ MinHeight=’50’ MaxHeight=’150’ /> </Grid.RowDefinitions> Первый столбец занимает большую часть места, оставляя второму ровно столько, чтобы поместилась кнопка Send: <Grid.ColumnDefinitions> <ColumnDefinition Width=’*’ /> <ColumnDefinition Width=’Auto’ /> </Grid.ColumnDefinitions> Панель Grid 205 Рис. 4.26. Моделирование интерфейса MSN Messenger с помощью сетки После определения строк и столбцов нам предстоит разместить элементы уп# равления. Прежде всего, отметим, что область разговора занимает несколько столбцов. В классе Grid определено четыре присоединенных свойства для дочер# них элементов: Row, RowSpan, Column и ColumnSpan. С помощью RowSpan и ColumnSpan можно указать, что элемент может занять несколько строк или столбцов: <Button Grid.ColumnSpan=’2’ MinWidth=’0’>Conversation</Button> <Button Grid.Row=’1’ MinWidth=’0’>TextEntry</Button> <Button Grid.Row=’1’ Grid.Column=’2’ MinWidth=’0’>Send</Button> Такое сочетание ограничений в определениях строк/столбцов и дочерних эле# ментов позволяет достичь желаемого поведения. Мало того, что начальное состо# яние правильно, но и при изменении размеров окна строки и столбцы ведут себя разумно (рис. 4.28). Глава 4. Размещение 206 Рис. 4.27. Эскиз желаемой организации строк и столбцов Изменить размер Рис. 4.28. Как меняется размещение при изменении размера сетки Элемент GridSplitter Если вам доводилось работать с MSN Messenger, то вы, наверное, заметили, что размер области ввода текста можно изменять; имеется небольшой раздели# тель, который дает пользователю шанс отвести больше места для печати (рис. 4.29). Описывая строки и столбцы в нашем примере, мы задали минимальный и максимальный размер второй строки, но не предоставили пользователю никакой возможности самому изменять размеры. GridSplitter – это обычный элемент управления (поддерживающий шаблоны и все прочее), который позволяет пользователю самостоятельно изменять разме# ры строк и столбцов во время работы программы. Его легко включить в наш при# мер, добавив лишь один элемент и задав некоторые свойства: <GridSplitter Margin=’0,0,0, 2.5’ Height=’5’ Панель Grid 207 ResizeDirection=’Rows’ ResizeBehavior=’CurrentAndNext’ Grid.Row=’0’ Grid.ColumnSpan=’2’ VerticalAlignment=’Bottom’ HorizontalAlignment=’Stretch’ /> Изменить размер Рис. 4.29. Изменение конфигурации MSN Messenger с помощью разделителя над областью текста Свойство ResizeDirection определяет, где расположен разделитель: между строками или между столбцами, а ResizeBehavior – какие именно строки и стол# бы подвергаются изменению. Есть два вида поведения при изменении размера: явное и неявное. Значение GridResizeBehavior.BasedOnAlignment задает неявное поведение; в этом случае разделитель анализирует выравнивание по вертикали и горизонтали, чтобы «догадаться», какие строки или столбцы изменять. Осталь# ные три значения (CurrentAndNext, PreviousAndCurrent и PreviousAndNext) поз# воляют явно указать те строки или столбцы, которые подлежат изменению (теку# щая и следующая, предыдущая и текущая, предыдущая и следующая). Изменить размер Рис. 4.30. Разделитель GridSplitter в действии 208 Глава 4. Размещение Поскольку мы не создали отдельной строки для разделителя, он занимает мес# то в одной из управляемых строк (первой), так что нужно редактировать «теку# щий» столбец. Разделитель находится в нижней части слота, а мы хотим изме# нять размер следующей (второй) строки, поэтому свойству ResizeBehavior прис# ваиваем значение CurrentAndNext (рис. 4.30). Так как все свойства выравнива# ния для разделителя установлены правильно, то этот пример будет работать ни# чуть не хуже и тогда, когда это свойство равно BasedOnAlignment. Grid – очень мощный, но при этом и весьма сложный менеджер размещения. С его помощью можно создать нетривиальный пользовательский интерфейс с большим числом вложенных элементов управления или реализовать собствен# ную логику размещения. Бывает, впрочем, что последнее – единственный выход. Реализация нестандартного размещения Чтобы написать собственный менеджер размещения, обычно создается класс, производный от Panel. Но прежде чем перейти к деталям, ненадолго отв# лечемся и поговорим о том, зачем вообще может понадобиться нестандартное размещение. Наиболее распространенных причины две: «алгоритмическое размещение» (желание расположить элементы, скажем, вдоль кривой) и производительность. Но ко второй причине следует относиться с осторожностью и не забывать фунда# ментальное правило: для оценки производительности необходимо тестирование. Часто программист думает, что приложение работает слишком медленно (или быстро), не протестировав его должным образом. Именно ради производитель# ности и был создан элемент UniformGrid; для равномерного распределения эле# ментов издержки Grid ни к чему. Алгоритмическое размещение более интересно. Предположим, что ряд эле# ментов управления хочется расположить по окружности (рис. 4.31). Ясно, что ни один из готовых менеджеров на это не рассчитан. Памятуя о двухэтапной модели размещения в WPF, мы должны начать с вы# числения предпочтительного размера контейнера. В данном случае это сопряже# но с некоторыми трудностями, в основном, из#за нетривиальной математики. Мы немного упростим алгоритм и воспользуемся моделью, показанной на рис. 4.32. Для вычисления предпочтительного размера проверим размер каждого по# томка и найдем максимальную высоту и ширину. Очень важно, чтобы метод Measure был вызван для каждого потомка9, иначе позабытые потомки не будут нарисованы: protected override Size MeasureOverride(Size availableSize) { double maxChildWidth = 0.0; double maxChildHeight = 0.0; // Измерить каждого потомка; в данном случае мы не налагаем 9 Мы пользуемся свойством InternalChildren, которое предназначено специально для разработки менеджеров размещения. Это свойство необходимо, если вы намереваетесь использовать панель внутри объекта ItemsControl или в другом контексте, где генерируются потомки (мы еще вернемся к этому вопросу в главе 6). Реализация нестандартного размещения 209 // на них ограничений. // foreach (UIElement child in InternalChildren) { child.Measure(availableSize); maxChildWidth = Math.Max(child.DesiredSize.Width, maxChildWidth); maxChildHeight = Math.Max(child.DesiredSize.Height, maxChildHeight); } // и далее... } Рис. 4.31. Пример алгоритмического размещения по окружности Максимальная высота Максимальная ширина 360°/ число потомков Рис. 4.32. Модель для вычисления предпочтительного размера при размещении по окружности 210 Глава 4. Размещение Зная максимальную ширину и высоту, можно вычислить идеальный размер панели, найдя радиус окружности, охватывающей все элементы. Но мы можем немного схитрить, так как распределяем элементы вдоль окружности равномерно: protected override Size MeasureOverride(Size availableSize) { double maxChildWidth = 0.0; double maxChildHeight = 0.0; // Измерить каждого потомка; в данном случае мы не налагаем // на них ограничений. // foreach (UIElement child in InternalChildren) { child.Measure(availableSize); maxChildWidth = Math.Max(child.DesiredSize.Width, maxChildWidth); maxChildHeight = Math.Max(child.DesiredSize.Height, maxChildHeight); } // Не идеальный алгоритм вычисления размера; мы вычисляем радиус // окружности, которая вмещает все элементы по ширине, а затем // вносим поправку на высоту. double idealCircumference = maxChildWidth * InternalChildren.Count; double idealRadius = (idealCircumference / (Math.PI * 2) + maxChildHeight); Size ideal = new Size(idealRadius * 2, idealRadius * 2); // и далее... } Последний шаг – попытаться «уложиться» в отведенную область. Поскольку мы го# товы смириться с перекрытием элементов, то нас устроит любой ее размер. Но в некото# рых случаях (например, если наш менеджер вложен в StackPanel), ширина или высота (или то и другое) может оказаться бесконечной. Важно не забывать про эту возможность, поскольку возвращать бесконечный предпочтительный размер запрещено: protected override Size MeasureOverride(Size availableSize) { double maxChildWidth = 0.0; double maxChildHeight = 0.0; // Измерить каждого потомка; в данном случае мы не налагаем // на них ограничений. // foreach (UIElement child in InternalChildren) { child.Measure(availableSize); maxChildWidth = Math.Max(child.DesiredSize.Width, maxChildWidth); maxChildHeight = Math.Max(child.DesiredSize.Height, maxChildHeight); } // Не идеальный алгоритм вычисления размера; мы вычисляем радиус // окружности, которая вмещает все элементы по ширины, а затем // вносим поправку на высоту. double idealCircumference = maxChildWidth * InternalChildren.Count; double idealRadius = Реализация нестандартного размещения 211 (idealCircumference / (Math.PI * 2) + maxChildHeight); Size ideal = new Size(idealRadius * 2, idealRadius * 2); // Вычисляем собственный размер, помня о том, что можем получить // бесконечное значение по любому направлению. В таком случае мы // вернем значение с семантикой «вплоть до» нашего идеала, но // согласимся на любые навязанные ограничения. // Size desired = ideal; if (!double.IsInfinity(availableSize.Width)) { if (availableSize.Width < desired.Width) { desired.Width = availableSize.Width; } } if (!double.IsInfinity(availableSize.Height)) { if (availableSize.Height < desired.Height) { desired.Height = availableSize.Height; } } return desired; } Реализация этапа установки несколько хитрее. Поскольку мы хотим разме# щать потомков по окружности (а не по эллипсу), то необходимо вычислить мак# симальный квадрат, который поместится в отведенной области. Для выполнения самого преобразования поворота у нас есть метод RotateTransform, но центр по# ворота придется найти самостоятельно. Все вместе приводит нас к модели, изоб# раженной на рис. 4.33. Two Этап установки Two R Описанный прямоугольник размещения e at ot m or sf an Tr Two Центр поворота Угол поворота Рис. 4.33. Модель установки потомков по окружности На первом шаге вычисляем местоположение и размеры прямоугольника, опи# санного вокруг окружности размещения: Глава 4. Размещение 212 protected override Size ArrangeOverride(Size finalSize) { // Вычисляем прямоугольник, описанный вокруг окружности, вдоль которой // будут равномерно размещены потомки // Rect layoutRect; if (finalSize.Width > finalSize.Height) { layoutRect = new Rect( (finalSize.Width finalSize.Height) / 2 ,0 ,finalSize.Height ,finalSize.Height); } else { layoutRect = new Rect( 0 ,(finalSize.Height finalSize.Width) / 2 ,finalSize.Width ,finalSize.Width); } // и далее... } Угол поворота вычисляется тривиально, надо лишь разделить полный угол на число потомков: protected override Size ArrangeOverride(Size finalSize) { // Вычисляем прямоугольник, описанный вокруг окружности, которой // будут равномерно размещены потомки // Rect layoutRect; if (finalSize.Width > finalSize.Height) { layoutRect = new Rect( (finalSize.Width finalSize.Height) / 2 ,0 ,finalSize.Height ,finalSize.Height); } else { layoutRect = new Rect( 0 ,(finalSize.Height finalSize.Width) / 2 ,finalSize.Width ,finalSize.Width); } вдоль double angleInc = 360.0 / InternalChildren.Count; // и далее... } Что здесь происходит? Во#первых, для каждого потомка необходимо вызвать метод Arrange. Если этого не сделать, потомок не будет нарисован. Метод Arrange Реализация нестандартного размещения 213 принимает на входе прямоугольник, который описывает окончательный размер и положение потомка. В данном случае центр элемента совмещается с центром это# го прямоугольника: protected override Size ArrangeOverride(Size finalSize) { // Вычисляем прямоугольник, описанный вокруг окружности, которой // будут равномерно размещены потомки // Rect layoutRect; if (finalSize.Width > finalSize.Height) { layoutRect = new Rect( (finalSize.Width finalSize.Height) / 2 ,0 ,finalSize.Height ,finalSize.Height); } else { layoutRect = new Rect( 0 ,(finalSize.Height finalSize.Width) / 2 ,finalSize.Width ,finalSize.Width); } вдоль double angleInc = 360.0 / InternalChildren.Count; double angle = 0; foreach (UIElement child in InternalChildren) { Point childLocation = new Point( layoutRect.Left + ((layoutRect.Width child.DesiredSize.Width) / 2) ,layoutRect.Top); child.Arrange(new Rect(childLocation,child.DesiredSize)); } // и далее... } После того как положение потомка установлено, можно применить метод RotateTransform для поворота его вокруг центра прямоугольника: protected override Size ArrangeOverride(Size finalSize) { // Вычисляем прямоугольник, описанный вокруг окружности, которой // будут равномерно размещены потомки // Rect layoutRect; if (finalSize.Width > finalSize.Height) { layoutRect = new Rect( (finalSize.Width finalSize.Height) / 2 ,0 вдоль Глава 4. Размещение 214 ,finalSize.Height ,finalSize.Height); } else { layoutRect = new Rect( 0 ,(finalSize.Height finalSize.Width) / 2 ,finalSize.Width ,finalSize.Width); } double angleInc = 360.0 / InternalChildren.Count; double angle = 0; foreach (UIElement child in InternalChildren) { Point childLocation = new Point( layoutRect.Left + ((layoutRect.Width child.DesiredSize.Width) / 2) ,layoutRect.Top); // Центр поворота находится в точке (0,0) относительно уже // установленного положения потомка. // child.RenderTransform = new RotateTransform( angle, child.DesiredSize.Width / 2, finalSize.Height / 2 layoutRect.Top); angle += angleInc; child.Arrange(new Rect(childLocation,child.DesiredSize)); } } Хотя реализовывать круговое размещение приходится не так уж часто, на этом примере мы продемонстрировали многие интересные аспекты написания нестандартного менеджера размещения10. Чего мы достигли В этой главе мы рассмотрели основополагающие принципы размещения и библиотеку менеджеров размещения. Мы видели, что приложение собирается из элементов управления, а менеджеры размещения играют важнейшую роль при определении местоположения и размеров этих элементов. Наличие развитой сис# темы размещения позволяет строить интерфейсы, способные динамически ме# нять размеры в зависимости от действия пользователя, размера экрана или изме# няющегося содержимого. 10 Единственная достойная упоминания особенность нестандартных менеджеров размещения, ко) торая осталась не рассмотренной, ) это размещение, зависящее от некоторого свойства (или нес) кольких свойств), как, например, DockPanel. Определяя собственные свойства, мы можем указать, что они влияют на этап измерения, установки и рисования. Более подробно этот вопрос рассмат) ривается в приложении. 215 Глава 5. Визуальные элементы Обсуждая концепции Windows Presentation Foundation, мы рассмотрели при ложения и способы построения пользовательских интерфейсов из элементов уп равления, а также применение менеджеров размещения для позиционирования элементов. Наша следующая тема – внутреннее устройство элементов управле ния. Точнее нас будет интересовать, как создаются их визуальные образы. Ведь в конечном итоге любой элемент управления сводится к набору визуальных эле ментов, которые выводят на экран пиксели. В основу разработки WPF положен фундаментальный принцип интеграции. Мы хотели создать единый комплект технологий, позволяющий решать широ кий спектр задач: от традиционных пользовательских интерфейсов до докумен тов и мультимедиа. Упомянутый принцип красной нитью проходит через весь комплект, но особенно наглядно проявляется на уровне визуализации. Эту гла ву мы начнем с рассмотрения двумерной графики, а затем перейдем и к трехмер ной. Познакомившись с основами, мы займемся изучением документов, вклю чая базовый текст, типографические аспекты и поток текста. Разобравшись с фундаментальными способами вывода пикселей (для визуализации двумерной и трехмерной графики и текста), мы сможем поговорить об анимации – способе оживить визуальные элементы. И последним пунктом этого путешествия будет область мультимедиа, в которой интересно сочетаются двумерная графика и анимация. Двумерная графика В WPF есть три уровня двумерной графики: фигуры, рисунки и команды ри сования. Команды рисования – это низкоуровневые инструкции системе компо зиции, рисунки – тонкая обертка вокруг команд рисования, а фигуры – готовые элементы, представляющие конкретные рисунки. Иными словами, рисунки – это объектноориентированное представление команд рисования, а фигуры наделя ют рисунки идентичностью, интерактивностью и стилями. Все двумерные опера ции сводятся к геометрическим примитивам, кисти и перу. Геометрия описывает форму фигуры, кисть служит для закрашивания внутренней области, а перо оп ределяет линию, которой проводится граница. Вот четыре основных геометри ческих примитива: линия, прямоугольник, эллипс и путь (рис. 5.1). Самым слож ным является путь; он позволяет описать произвольный контур, состоящий из прямых и кривых линий. С помощью пути можно описать и любой из прочих ге ометрических примитивов. Глава 5. Визуальные элементы 216 Принципы двумерной графики В основу всех слоев системы композиции положен единый набор принципов. В такой системе, как WPF, где высокоуровневые слои (например, элементы уп равления) напрямую опираются на слои нижних уровней, эти принципы нагляд но проявляются на верхнем уровне. Композитный рендеринг В любом графическом ядре есть два способа реализовать кооперативный ренде ринг: отсечение и композиция. Библиотека GDI, а еще раньше User32 базировалась на системе отсечения. Каждому элементу выделялась прямоугольная область, в которой только и осуществлялось рисование. Отсечение позволяло оптимизиро вать производительность, главным образом, потому что любой элемент можно бы ло нарисовать, почти ничего не зная о других элементах. Когда операционная сис тема Windows только создавалась, эта оптимизация была критически важна – лю бое окно можно было нарисовать, не перерисовывая все остальные окна. Прямоугольник Эллипс Линия Путь Рис. 5.1. Четыре основных графических примитива С другой стороны, композитная система позволяет рисовать элементы поверх друг друга. В случае GDI/User32 такого эффекта можно было отчасти добиться с помощью флага CS_PARENTDC. Вместо того чтобы производить отсечение для команд рендеринга, мы выполняем операции рисования, начиная с самого ниж него элемента, поэтому элементы с меньшими значениями zиндекса перекрыва ются более «близкими». Это позволяет без труда реализовывать, например, по лупрозрачность и непрямоугольные элементы. В WPF для всех элементов реализована композитная система рендеринга. Од нако на самом верхнем уровне необходимо создать окно в смысле User32, которое могло бы взаимодействовать с другими частями операционной системы. Компо Двухмерная графика 217 зицию можно до некоторой степени поддержать за счет описанного выше меха низма уровней окон, предоставляемого ОС. Кстати, в Windows Vista он сущест венно усовершенствован. Независимость от разрешающей способности В WPF не существует способа определить, что такое реальный пиксель. Од ним из принципов проектирования WPF был постулат о независимости от разре шающей способности, то есть приложение должно было работать одинаково при любом разрешении устройства и во всех случаях выглядеть оптимально. Незави симость от разрешающей способности проявляется в разных формах: от интен сивного использования векторной графики до механизма размещения и т.д. Од ним из основных способов реализации такой независимости в WPF является применение не зависящей от разрешения системы координат, не основанной на физических пикселях. В GDI система координат опирается на пиксели. Когда мы говорим, что одна точка отстоит от другой на 5 пикселей, это означает, что на устройстве вывода между этими точками будет ровно пять пикселей. Конечно, в GDI есть и другие системы координат1, но в конечном итоге все сводится к пикселям. В WPF в качестве единицы измерения длины используется логический пик сель, равный 1/96 дюйма. Сразу же возникает вопроса: почему именно 1/96? Яс но, что 1 сантиметр или 1 дюйм выглядел бы куда естественнее. Но разработчики уже долгое время пользуются пикселями в смысле GDI, а разрешающая способ ность большинства устройств вывода равна 96 dpi (точек на дюйм), поэтому 1/96 дюйма не только отвечает интуитивным ожиданиям программистов, но и соотве тствует современным физическим устройствам. Я уверен, что по мере повыше ния разрешающей способности мониторов2 это решение будет казаться все более и более произвольным3. Для согласования с логической картиной мира в WPF при рендеринге пред полагается бесконечная разрешающая способность. В частности, это означает, что текст прорисовывается одинаково вне зависимости от размера или разреше ния устройства вывода, поскольку внутренние вычисления производятся с бес конечной точностью. (Ну, конечно, не бесконечной, но достаточно высокой!) Од нако при работе с устройствами, имеющими относительно низкое разрешение, например, компьютерными экранами, возникают сложности. Есть два общепринятых способа создания качественного изображения на эк ранах с небольшой разрешающей способностью, имеющихся у большинства пользователей. Алгоритм сглаживания (antialiasing) выполняет все вычисления, ориентируясь на высокое разрешение, а при выводе на экран определяет, сколь 1 2 3 В Visual Basic в качестве единицы измерения используются твипы (twips). Пунктом (point) на зывается 1/72 часть дюйма, а твип — это 1/20 часть пункта, то есть 1/1440 дюйма. Разрешающая способность оригинальных мониторов Macintosh составляла 72 dpi в соответ ствии с определением типографического пункта. В современной типографии пункт равен в точности 1/72 дюйма, поскольку именно такое опре деление принято в большинстве приложений для набора текста, хотя исторически пункт был чуть меньше. Глава 5. Визуальные элементы 218 ко логических линий содержится в каждом пикселе, и соответственно подсвечи вает его. Технология ClearType улучшает эту модель, принимая во внимание вы равнивание отдельных цветовых сегментов на ЖКпанелях, что утраивает коли чество адресуемых пикселей на устройстве вывода, но по существу это все тот же трюк с «взвешиванием» линий. Обоим подходам свойственна одна и та же проблема – изображение часто кажет ся размытым. Для текста и векторной графики этот эффект в общем случае даже же лателен. Применение технологии ClearType к тексту существенно улучшает его восприятие, поскольку литеры становятся более плавными. Но для элементов гра фического интерфейса, например кнопки внутри сетки, мы хотим получить четкие края. Для решения этой проблемы в WPF введено понятие совмещения с пикселя ми (pixel snapping). Рендеринг попрежнему выполняется в логической системе ко ординат, но при выводе на устройство линии точно ложатся на пиксели (рис. 5.2). 1 - С совмещением 2 - Без совмещения Рис. 5.2. Прямоугольники крупным планом при включенном и отключенном режиме совмещения с пикселями. Обратите внимание, насколько четким получается сов мещенное изображение, несмотря на то, что контур прямоугольника проходит не точно по границам пикселей. В общем случае нет необходимости изменять умолчания для включения ре жима совмещения; но при необходимости для явного управления им служит свойство SnapsToDevicePixels. Геометрические преобразования На самом базовом уровне системы рендеринга в WPF нет понятия «позиции» элемента. Вместо использования координат x и y система полагается на геометри ческие преобразования, применяемые к точкам (напомним, что в конечном итоге все сводится к геометрии). Самыми употребительными являются преобразования TranslateTransform (перенос), ScaleTransform (гомотетия) и RotateTransform (по ворот). С их помощью мы можем изменять размер и положение элемента, а так же вращать его вокруг некоторой точки. Внутри WPF выражает любую информа цию о положении элемента в терминах преобразования TranslateTransform. Двухмерная графика 219 Преобразования можно комбинировать, то есть строить из них цепочки, нап ример: сдвинуть элемент на 5 пикселей вправо, затем растянуть с коэффициен том 3 и повернуть на 15 градусов: <Button> <Button.RenderTransform> <TransformGroup> <TranslateTransform X=’5’ /> <ScaleTransform ScaleX=’2’ ScaleY=’2’ /> <RotateTransform Angle=’15’ /> </TransformGroup> </Button.RenderTransform> </Button> Геометрические примитивы Прежде чем приступать к обсуждению геометрических примитивов, кистей, перьев и других деталей рисования пикселей на экране, зададимся вопросом, за чем все это нужно знать. Лично для меня это как понимание грамматики языка. В случае естественного языка – того, на котором мы говорим с детства, – мы обычно не вникаем в детали грамматических правил, а просто знаем, что пра вильно, а что – нет. Когда я впервые начал изучать иностранный язык, то многое узнал и об английском. Я стал понимать, как применяются правила. Понимание того, как работают основные конструкции графического интерфейса, важно по той же причине: наверняка вы в прошлом работали с другими графическими сис темами, и у каждой были свои особенности, правила и возможности, о которых вы уже сознательно и не думаете. Геометрические примитивы – это базовые строительные блоки всей двумер ной графики в WPF. Любой рисунок представляется в виде последовательности графических примитивов, которые нужно нарисовать. А, стало быть, полезно по нимать, как эта система работает. Все примитивы, по существу, являются частны ми случаями PathGeometry, поэтому имеет смысл потратить время на знакомство с путями. Путь – это последовательность фигур. Каждая фигура составлена из набо ра сегментов. В какомто смысле сегменты можно уподобить программе «че репашьей графики», с которой я работал на своем стареньком компьютере Apple II (команды MoveTo, LineTo и т.д.). Сегмент – это команда переместить перо по экрану. Чтобы разобраться с этим, рассмотрим, из каких сегментов составлены фигуры, которые в совокупности дают изображение, показанное на рис. 5.3. Для начала нужно задать начальную точку фигуры. Простейшим сегментом является LineSegment, который рисует отрезок прямой из текущей точки (поме ченной на рис. 5.3 как «StartPoint») в указанную («Line.Point»). Чтобы увидеть, как это происходит, включим в путь Path примитив, который мы определяем. Если пометить фигуру признаком IsClosed, то она будет содер жать отрезок, соединяющий конечную точку с начальной: 220 Глава 5. Визуальные элементы Рис. 5.3. Сложный путь, составленный из всех сегментов нескольких фигур <Path Width=’200’ Height=’200’ Fill=’#111111’ Stroke=’Black’ StrokeThickness=’2’> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigure StartPoint=’5,5’ IsClosed=’True’> <LineSegment Point=’90,5’ /> </PathFigure> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path> Следующий интересный сегмент – это BezierSegment, который позволяет оп ределить кривую Безье с помощью двух контрольных точек (Point1 и Point2) и конечной точки (Point3). Этот и аналогичные ему сегменты названы на рис. 5.3 «Bezier.Point1»: <PathFigure StartPoint=’5,5’ IsClosed=’True’> <LineSegment Point=’90,5’ /> <BezierSegment Point1=’195,5’ Point2=’100,40’ Point3=’160,90’ /> </PathFigure> Сегмент ArcSegment определяет дугу эллипса заданного размера до указанной точки («Arc.Point» на рис. 5.3): <PathFigure StartPoint=’5,5’ IsClosed=’True’> <LineSegment Point=’90,5’ /> <BezierSegment Point1=’195,5’ Point2=’100,40’ Point3=’160,90’ /> <ArcSegment Point=’50,130’ Size=’65,55’ IsLargeArc=’false’ SweepDirection=’Clockwise’ /> </PathFigure> И, наконец, QuadraticBezierSegment («QBezier» на рис. 5.3) – это упрощенный вариант BezierSegment, требующий только одной контрольной точки (Point1): Двухмерная графика 221 <PathFigure StartPoint=’5,5’ IsClosed=’True’> <LineSegment Point=’90,5’ /> <BezierSegment Point1=’195,5’ Point2=’100,40’ Point3=’160,90’ /> <ArcSegment Point=’50,130’ Size=’65,55’ IsLargeArc=’false’ SweepDirection=’Clockwise’ /> <QuadraticBezierSegment Point1=’5,45’ Point2=’40,40’ /> </PathFigure> В составе одного пути может быть не только произвольное число сегментов, но и фигуры. Эта возможность особенно полезна, потому что любая кисть закра шивает весь путь, следовательно, мы можем задать плавный градиент, распрост раняющийся на несколько фигур: <Path ...> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigure StartPoint=’5,5’ IsClosed=’True’> ... </PathFigure> <PathFigure StartPoint=’150,40’ IsClosed=’True’> <LineSegment Point=’180,40’ /> <LineSegment Point=’180,60’ /> <LineSegment Point=’150,50’ /> </PathFigure> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path> Последние два геометрических примитива – комбинирующие. Это CombinedGeometry и GeometryGroup. Примитив GeometryGroup принимает список потомков и применяет к ним композицию. Свойство FillRule определяет, какие части получающейся фигуры закрашивать. GeometryGroup может содер жать любое число дочерних примитивов, причем при любых условиях контур каждого примитива рисуется независимо. CombinedGeometry позволяет объединить ровно два примитива, причем спо соб создания из них новой фигуры определяется свойством GeometryCombineMode (на рис. 5.4 показаны различные варианты). Так как CombinedGeometry создает одну новую фигуру, то прорисован будет лишь внеш ний контур последней. Рис. 5.4. Разные способы комбинирования геометрических примитивов 222 Глава 5. Визуальные элементы Обычно сложные геометрические формы создаются с помощью инструмен тальных программ, потому что вручную ни описать, ни сопровождать их впосле дствии практически нереально. Но, усвоив базовые принципы, вы будете знать, что возможно и какое эффекты достижимы. Цвет Поняв, как создавать геометрические фигуры, разберемся с тем, как рисуются пиксели. Раз двумерный рисунок состоит из геометрических примитивов, кистей и перьев, то прежде всего надо познакомиться с концепцией цвета. Какого цвета зеленый? Как обозначить конкретный цвет? В Webпрограммировании зеленый цвет называется «Green» или «#00FF00». В терминологии GDI его код 32768. То и другое – правильные обозначения цвета, но ни одно из них не говорит мне, како го же он цвета. Попробуем подойти к проблеме подругому. Как можно показать комуни будь, что такое зеленый цвет? Выйти на улицу и показать пальцем на клочок тра вы. Конечно, цвет травы зависит от освещения, в солнечные дни она кажется яр че. Можно еще указать этот цвет на экране монитора. Мониторы (как, впрочем, и все цветопередающие устройства) способны отображать лишь ограниченное ко личество цветов. Это ограничение обусловлено физической природой устройства и относится в равной мере и к мониторам, и к принтерам, и к телевизорам, и к цифровым камерам, и к сканерам – ко всякому прибору, который может считы вать или отображать цвета. Таким образом, у нас нет способов ни точно закодировать цвет, ни точно по казать его. Теперь становится понятно, насколько проблема цвета на самом деле сложна. В любой книге, посвященной высококлассным программам для редакти рования цветных изображений (типа Adobe Photoshop), уделено много внимания цвету – пространствам цветов, моделям, рабочим моделям и т.д. А какое нам, собственно, до этого дело? По мере того как все больше и больше решений принимается на основе ис пользования компьютера, способность сохранять оригинальные цвета оказывает ся все более важной. Понятно ведь, что цвета логотипа компании на Webсайте и в печатном буклете должны совпадать. Другой пример: насколько близки к ре альным должны быть цвета на фотографии? А если эту фотографию включить в документ, то должны ли цвета сохраниться? Проблема цветопередачи в докумен тообороте и сохранности всей цветовой информации становится значимой для всех, кто имеет дело с цифровыми документами. Описание цвета Мы уже говорили, что для описания цвета нужна какаято схема кодирования. Кроме того, необходима информация о профиле кодирования. Цветовой про филь содержит информацию, позволяющую программам переходить от одной Двухмерная графика 223 кодировки к другой. На сегодняшний день основные цветопередающие устрой ства – это компьютерные мониторы (ЭЛТ или ЖК) и принтеры (в которых при меняются самые разные технологии печати). 4 Самым популярным форматом описания цвета является sRGB . Эта модель естественна для большинства мониторов и по умолчанию применяется в Web. В схеме sRGB есть три канала данных, каждый шириной 8 бит. В совокупности мы получаем 24 бита, которые позволяют представить примерно 16.7 миллионов цветов. Учитывая, что человеческий глаз способен различить лишь около 8 мил лионов цветов, такая расточительность достойна осуждения. Проблема в том, что стандарт sRGB описывает компьютерную цветовую гамму5. Каждая схема кодирования способна представить определенную цветовую гамму. Насколько черным является черный цвет, а насколько белым – белый? В кодировке sRGB можно представить 16.7 миллиона цветов, но полный диапазон при этом относительно невелик6. Таким образом, мы имеем весьма мелкую шка лу цветов, то есть шаг между соседними оттенками мал. Если распространить схе му sRGB на более широкий диапазон цветов, то шаг между оттенками окажется слишком велик. Рис. 5.5. Различные цветовые профили, показывающие оттенки серого 4 5 6 Буква s означает standard (стандарт), а RGB – redgreenblue (красныйзеленыйсиний). Эта цветовая модель документирована организацией W3C (World Wide Web Consortium) на страни це www.w3.org/Graphics/Color/sRGB. Термин цветовая гамма относится к числу представимых цветов. Есть гамма, которую спосо бен воспринять глаз человека, есть гамма, которую может воспроизвести устройство отобра жения, а есть гамма, которую можно описать схемой кодирования цветов. Когда говорят, что нечто имеет «компактную гамму», имеют в виду способность представить лишь малую часть спектра возможных цветов. Типичная система оценивания в школах дает еще один пример гаммы. В этой шкале есть оценки от 1 до 5, то есть всего пять различных значений: 1, 2, 3, 4, 5. Точность шкалы можно увеличить, введя оценки 5, 5, 4+, 4, 4 и т.д. Можно и еще расширить гамму, добавив оценки 5+ и 1. Расширяя гамму, мы можем представить оценки за пределами диапазона 15. 224 Глава 5. Визуальные элементы 7 Для решения этой проблемы был предложен стандарт scRGB . В этой кодиров ке цвета представлены числами с плавающей точкой переменной длины. Один цвет может быть закодирован 64 или 128 битами (вместо 24 бит в sRGB). Кроме того, гамма scRGB очень широка (далеко выходит за пределы человеческого зре ния). Чтобы понять, зачем scRGB допускает такую высокую точность представле ния цвета и такую широкую гамму, нужно вернуться к проблеме цветопередачи при документообороте. В силу повышенной точности можно выполнять преобра зования цветов (например, затемнять изображение), не теряя важных данных. WPF на уровне ядра поддерживает как sRGB, так и scRGB. К тому же, разре 8 шается использовать любой цветовой профиль ICC . На рис. 5.5 показаны профи ли scRGB, sRGB, а также нестандартный профиль Adobe 1998 (очень похожий на sRGB); во всех случаях представлена кодировка цвета «White»: <Rectangle Fill=’sc# 1.0,1.0, 1.0, 1.0’ /> <Rectangle Fill=’#FFFFFFFF’ /> <Rectangle Fill=’ContextColor file://.../AdobeRGB1998.icc 1.0,1.0,1.0,1.0’ /> Отметим, что во всех этих кодировках участвуют четыре значения: Red, Green, Blue и Alpha. WPF поддерживает альфаканал для всех цветов, что позволяет за давать степень прозрачности цвета: 0 означает полностью прозрачный, а 255 – полностью непрозрачный. В цветовых профилях могут применяться и другие способы кодирования альфаканала (равно как и самих цветов). Так, в sRGB аль фа представляется 8 битами (0255), а в scRGB – 16 битами (0.0 – 1.0; оставшие ся биты числа с плавающей точкой в scRGB используются для документооборо та). Мы еще вернемся к деталям работы с альфаканалом и способам его исполь зования для управления прозрачностью в WPF. Кисти Как бы интересна ни была тема определения цвета, одногоединственного цве та недостаточно для рисования. Кисть – это способ сообщить системе, как раскра шивать пиксели внутри некоторой области. Иногда областью является внутрен ность пути, а иногда – штрих. В WPF определено шесть кистей: SolidColorBrush, 7 8 Тайна происхождения аббревиатуры scRGB покрыта мраком. Официально она не означает ни чего. По словам Майкла Стокса (Michael Stokes – глава национальной и Международной элект ротехнической комиссии (IEC), работающей над стандартом scRGB), это название появилось, когда японский национальный комитет попросил изменить предыдущий вариант XsRGB (excess RGB). Есть два основных кандидата на расшифровку: «specular RGB» (RGB с отраже нием), поскольку scRGB поддерживает больше белых цветов, чем просто значения 1.0, озна чающие полное рассеивание, и «standart compositing RGB» (стандартный композитный RGB) изза линейности, поддержки чисел с плавающей точкой, технологии HDR (high dynamic range – расширенный динамический диапазон) и наличия широкой гаммы, которая идеально подхо дит для композиции. Последняя расшифровка к тому же неявно подчеркивает, что scRGB не предназначен для прямой поддержки в аппаратных устройствах или форматах, поскольку по определению охватывает значения за пределами человеческого зрения и (даже теоретичес ки) реализуемых физических приборов. International Color Consortium (Международный консорциум по средствам обработки цветных изображений – www.color.org) определяет единый формат цветовых профилей. Двухмерная графика 225 LinearGradientBrush, RadialGradientBrush, ImageBrush, DrawingBrush и VisualBrush. Важно помнить, что в любом месте, где можно задать простой цвет, можно задать и кисть. На самом деле, когда в разметке указывается цвет, система создает кисть типа SolidColorBrush. Градиентные кисти Направление градиента Кисти LinearGradientBrush и RadialGradientBrush (рис. 5.6) позволяют осу ществлять градиентную заливку. LinearGradientBrush применяет алгоритм сме шения цветов вдоль вектора, а RadialGradientBrush – радиально, начиная с ука занной точки. Центр и радиус окружности Рис. 5.6. Основные атрибуты градиентных кистей двух типов Рис. 5.7. Эффекты применения различных атрибутов RadialGradientBrush Работа с кистью RadialGradientBrush осложняется изза взаимодействия че тырех свойств: GradientOrigin, Center, RadiusX и RadiusY. Последние три опреде ляют эллипс, внутри которого применяется градиент. Свойство GradientOrigin интерпретируется довольно странно; оно говорит, где должна находиться фокус ная точка градиента. Если расположить фокус вне границ эллипса, то мы полу 226 Глава 5. Визуальные элементы чим очень необычные изображения. На рис. 5.7 показано, как задание разных зна чений этих свойств влияет на заливку кистью RadialGradientBrush. В каждом примере прямоугольник залит градиентно и обведен черной кистью. В последнем случае (All) показано, что фокусная точка градиента может лежать за пределами элемента. Для управления градиентом есть много возможностей, но помимо уже рас смотренных выше интерес представляет еще лишь свойство SpreadMethod. Оно определяет, что должно происходить, когда мы достигаем конца градиента. Если задать значение по умолчанию Pad, то будет взята последняя точка градиента и этим цветом закрашена оставшаяся часть фигуры. Значение Reflect создает зер кальное отражение градиентной заливки, а значение Repeat просто повторяет ее. На рис. 5.8 показано, как с помощью свойства SpreadMethod можно создавать сложные изображения. Рис. 5.8. Применение разных значений свойства SpreadMethod к радиальной и линейной градиентным кистям Для кистей любого вида надо понимать, для чего нужно свойство MappingMode. С его помощью мы можем растянуть кисть, так чтобы она запол нила всю область, или оставить фиксированный размер. Если MappingMode рав но Absolute, то размер кисти фиксирован. Чтобы растянуть градиент, следует за дать значение RelativeToBoundingBox; оно означает, что система координат опре делена относительно охватывающего прямоугольника. Это пространство разби вается на части путем задания числа от 0.0 до 1.0. На рис. 5.9 показано несколько примеров применения свойства MappingMode. Двухмерная графика 227 Рис. 5.9. Сравнение режимов отображения RelativeToBoundingBox и Absolute Рис. 5.10. Рамка окна в стиле Windows XP крупным планом У градиентных кистей есть свойство MappingMode, которое управляет общим поведением кисти. Свойства GradientStop всегда задаются относительно области, которая заливается градиентом (аналогично RelativeToBoundingBox). Чтобы по нять, почему это так важно, рассмотрим создание рамки окна в стиле Windows XP (рис. 5.10). Верхняя полоса закрашена градиентной кистью фиксированного разме ра и имеет скругленные углы. Для достижения такого эффекта мы можем восполь зоваться элементом управления Border с кистью LinearGradientBrush (рис. 5.11): <Border CornerRadius=’5,5,0,0’ BorderBrush=’#FF0144D0’ BorderThickness=’1’ > <Border.Background> <LinearGradientBrush Глава 5. Визуальные элементы 228 MappingMode=’Absolute’ StartPoint=’0,0’ EndPoint=’0,24’ > <LinearGradientBrush.GradientStops> <GradientStop Offset=’0’ Color=’#FF0058EB’ /> <GradientStop Offset=’0.03’ Color=’#FF3D95FF’ /> <GradientStop Offset=’.06’ Color=’#FF2B90FF’ /> <GradientStop Offset=’.4’ Color=’#FF0055E5’ /> <GradientStop Offset=’.6’ Color=’#FF0055E5’ /> <GradientStop Offset=’.9’ Color=’#FF036EFF’ /> <GradientStop Offset=’1’ Color=’#FF0144D0’ /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Border.Background> <Rectangle Fill=’White’ Margin=’2,24,2,2’ /> </Border> Рис. 5.11. Применение режима отображения Absolute для создания градиента фик сированного размера Выбрать содержимое Исходное изображение (800x600) Масштабировать изображение Viewbox Залить Viewport TileMode Рис. 5.12. Применение свойств Viewbox, Viewport и TileMode для заливки частью су ществующего изображения Обратите внимание, что опорная точка градиента GradientStop вычисляется относительно охватывающего прямоугольника кисти, который был определен в Двухмерная графика 229 абсолютных координатах: от (0,0) до (0,24). Это означает, что вторая опорная точка находится на расстоянии, равном 0.03 от 24, то есть отстоит примерно на 0.72 пикселя от верхней границы. Мозаичные кисти Кисти ImageBrush, DrawingBrush и VisualBrush являются мозаичными, то есть растягивают или повторяют некоторое изображение (определенное с по мощью растровой или векторной графики либо в виде полного дерева отображе ния), чтобы замостить им указанную область. Работа с мозаичной кистью подра зумевает выполнение трех операций (рис. 5.12): (1) выбор содержимого, которое нужно повторять (с помощью свойств Viewbox и ViewboxUnits), (2) масштабиро вание этого содержимого (с помощью свойств Viewport, ViewportUnits, Stretch, AlignmentX и AlignmentY) и (3) заполнение области содержимым свойства Viewport (так, как предписывает свойство (TileMode). Кисть ImageBrush принимает в качестве источника любое изображение9, а DrawingBrush – любой векторный рисунок (мы еще поговорим об этом ниже). Но, пожалуй, наибольший интерес представляет кисть VisualBrush. Рис. 5.13. Заполнение нескольких прямоугольников с помощью кисти VisualBrush Кисть VisualBrush принимает в качестве источника любой элемент управления (точнее, объект любого класса, производного от System.Windows.Media.Visual). 9 Интересно отметить, что поскольку ImageBrush может принять в качестве источника вектор ный рисунок, то для заполнения области таким рисунком годится как ImageBrush, так и DrawingBrush. Глава 5. Визуальные элементы 230 Напомним, что VisualBrush использует визуальный образ элемента, но не поддер живает интерактивность. Щелчок мышью по прямоугольнику, заполненному изображениями кнопки, не приведет к нажатию этой кнопки. Вроде бы очевидно, но, запустив такое приложение, как на рис. 5.13, об этом легко забыть! public class VisualBrushSample : Window { public VisualBrushSample() { Title = «Visual Brush»; StackPanel sp = new StackPanel(); Button theButton = new Button(); theButton.HorizontalAlignment = HorizontalAlignment.Left; theButton.VerticalAlignment = VerticalAlignment.Top; theButton.Content = «Click Me!»; sp.Children.Add(theButton); Rectangle rect = new Rectangle(); rect.Margin = new Thickness(5); rect.Width = 200; rect.Height = 200; rect.Stroke = Brushes.Black; rect.StrokeThickness = 5; VisualBrush vb = new VisualBrush(); vb.Visual = theButton; vb.TileMode = TileMode.Tile; vb.Stretch = Stretch.Uniform; vb.Viewport = new Rect(0, 0, 50, 20); vb.ViewportUnits = BrushMappingMode.Absolute; rect.Fill = vb; sp.Children.Add(rect); Rectangle rect2 = new Rectangle(); rect2.Margin = new Thickness(5); rect2.Width = 500; rect2.Height = 200; rect2.Stroke = Brushes.Black; rect2.StrokeThickness = 5; VisualBrush vb2 = new VisualBrush(); vb2.Visual = theButton; rect2.Fill = vb2; sp.Children.Add(rect2); Content = sp; } } Отметим, что один и тот же визуальный элемент может служить источником для нескольких объектов VisualBrush. Кроме того, запустив эту программу, мы увидим, что при перемещении мыши по «настоящей» кнопке корректно подсвечи Двухмерная графика 231 ваются все ее двойники. VisualBrush – самая развитая из всех кистей просто пото му, что для выполнения заливки нам доступны все прочие возможности WPF. Перья Перья применяются для вычерчивания контура фигуры. Основными атрибута ми пера являются кисть и толщина. Кисть нужна для рисования края фигуры ли нией заданной толщины. Поскольку перу разрешается передавать любую кисть, мы можем комбинировать с пером мозаичное заполнение и градиентные кисти и соз давать очень сложные эффекты. Помимо этих двух свойств, можно задать способ рисования начала и конца линии, места соединения двух линий и пунктирность. Рис. 5.14. Фигуры, нарисованные пером. Каждая нарисована перьями трех разных толщин В большинстве случаев свойства пера раскрываются как свойства объемлющего элемента. Например, чтобы изменить характеристики пера, которым рисуется рам ка, мы пользуемся свойствами BorderBrush и BorderThickness, а для заливки прямо угольника – свойствами Stroke, StrokeThickness, StrokeDashArray и другими. Рис. 5.15. Различные способы нарисовать место соединения двух линий Геометрические точки не имеют размеров, место занимает только толщина пе ра. Эта толщина распределяется поровну по обе стороны математической линии, соединяющей две геометрические точки (рис. 5.14). Обратите внимание, что по мере увеличения толщины пера острые углы верхнего треугольника начинают далеко отступать от исходной линии. Чтобы воздействовать на этот аспект пове дения, можно задать для пера свойство LineJoin. Различные типы соединения ли ний не требуют подробных пояснений (рис. 5.15). Отметим, однако, что у значе ния Miter (Сглаживание) есть интересная особенность: применяя сглаженное со единение, мы можем с помощью дополнительного свойства MiterLimit указать, при 232 Глава 5. Визуальные элементы каких условиях линия должна сглаживаться. Это позволяет оставить четкие углы для правильных фигур (типа квадрата), а сглаживание применять только в случае, когда фигура начинает слишком далеко уходить в сторону (как острые углы). Рис. 5.16. Различные способы нарисовать окончание линии Свойство LineJoin описывает форму штриха в месте соединения двух линий (нап ример, в углах квадрата и треугольника), но часто необходимо управлять тем, как ри суются начало и конец линии (как, например, для кривой Безье в правом нижнем уг лу на рис. 5.14). Для этого служат свойства StartLineCap и EndLineCap (рис. 5.16). Еще одна интересная возможность изменения внешнего вида фигуры – задание пунктира. Свойство пера DashStyle позволяет включать и выключать пунктир. Для модификации окончаний штрихов применима та же модель, что и для линии в це лом. Отметим, что значения этого свойства вычисляются относительно толщины 10 линии: чем она толще, тем дальше друг от друга отстоят штрихи (рис. 5.17) . Рис. 5.17. Различные окончания штрихов. Используется один и тот же стиль штрихов, но разная толщина. 10 Плоское (Flat) и квадратное (Square) окончания выглядят очень похоже. Сравнивая их на рис. 5.16, мы видим, что по мере увеличения толщины пера окончание становится больше (то есть квадратные окончания выступают за пределы последней точки линии). Тот же феномен наблю дается и для окончаний штрихов: в случае квадратного окончания штрих оказывается несколько длиннее, чем в случае плоского. На практике отличить одного от другого довольно сложно. Двухмерная графика 233 Прежде чем закончить обсуждение перьев, напомним еще раз: перья основаны на модели кистей, с которой мы уже познакомились. Для проведения контура фигу ры можно использовать любую кисть. Можно применять градиентную заливку, мозаичное заполнение и даже визуальную кисть, а закончить контур линией, ко торая будет проигрывать видео! Рисунки Разобравшись с геометрическими примитивами, кистями и перьями, можно пе рейти к вопросу о том, как они используются для создания изображений. На самом низком уровне находятся рисунки (drawings). Рисунки – это API для прямого вза имодействия с низкоуровневым механизмом композиции. Поэтому им недостает некоторых функций (ввод, отслеживание фокуса, маршрутизация событий, систе ма размещения и т.д.), зато у них есть другие свойства (например, обобществле ние), которые и делают их весьма полезными. Концептуально любое изображение на экране представляется в виде последовательности объектоврисунков. Поскольку рисунки – это принципиально низкоуровневый механизм, им не хватает удобства, присущего работе с фигурами. Рисунки непосредственно раск рывают триаду примитивкистьперо, которая необходима для получения изоб ражения. Не существует рисунка прямоугольника, вместо этого мы создаем рису нок и назначаем ему прямоугольник в качестве геометрического примитива. Все идеи, которые мы рассматривали до сих пор, вращаются вокруг понятия древовидной иерархии. Так, любой элемент управления находится в какомто месте дерева отображения и может иметь одного родителя и одного или несколь ких потомков. Рисунок же обладает иной структурой, он представляется в виде графа. Один и тот же рисунок может присутствовать в нескольких вершинах гра фа, что дает заметный выигрыш в производительности. Рис. 5.18. Использование кисти DrawingBrush в сочетании с рисунком GeometryDrawing Глава 5. Визуальные элементы 234 Для демонстрации этой модели обобществления (рис. 5.18) определим один геометрический примитив и включим его в рисунок toShare; затем определим два объекта DrawingGroup, для каждого из которых toShare является потомком. Объ ект EllipseGeometry (и построенный на его основе объект GeometryDrawing) су ществует в единственном экземпляре, но может использоваться в двух разных контекстах, где каждый из объектов DrawingGroup пользуется этим рисунком, применяя к нему преобразование TranslateTransform: <!— Drawings.xaml —> <Window x:Class=’EssentialWPF.Drawings’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’Drawings’ > <Rectangle Name=’_rect’ /> </Window> // Drawings.xaml.cs public Drawings() { InitializeComponent(); GeometryDrawing toShare = new GeometryDrawing(Brushes.Red, null, new EllipseGeometry(new Rect(2, 2, 15, 15))); DrawingGroup main = new DrawingGroup(); DrawingGroup a = new DrawingGroup(); a.Children.Add(toShare); a.Transform = new TranslateTransform(8, 8); DrawingGroup b = new DrawingGroup(); b.Children.Add(toShare); b.Transform = new TranslateTransform(0, 0); main.Children.Add(b); main.Children.Add(a); DrawingBrush brush = new DrawingBrush(); brush.Drawing = main; brush.Viewport = new Rect(0, 0, 20, 20); brush.ViewportUnits = BrushMappingMode.Absolute; brush.TileMode = TileMode.Tile; _rect.Fill = brush; } На рис. 5.19 показано созданное для этого рисунка изображение. Рисунок toShare создается только один раз, что гарантирует очень эффективное рисова ние сложных объектов. Обобществление здесь гораздо глубже, чем просто раск Двухмерная графика 235 рытие объектной модели; поскольку рисунок не более, чем тонкая обертка вокруг ядра композиции, то обобществление производится внутри ядра. Фигуры Фигуры (shapes) привносят рисунки в мир элементов управления. Помимо функций, свойственных элементам (размещение, ввод данных, фокус, маршрутиза ция событий и т.д.), фигуры еще предоставляют ряд простых методов для создания общеупотребительных конструкций. Классы Ellipse, Line, Path и Rectangle взаимно однозначно соответствуют своим геометрическим собратьям. Классы же Polygon и Polyline – это простые обертки для конструирования путей определенного вида. Рис. 5.19. Граф рисунка, созданный для получения изображения на рис. 5.18 С фигурами связана одна интересная проблема. Для рисунков определена собственная система координат. С помощью сдвигов и преобразований мож но изобразить рисунок в разных позициях. Однако положение и размер эле ментов управления определяет менеджер размещения. Первый способ спра виться с размещением – воспользоваться кистью DrawingBrush, то есть при менить к рисунку геометрическое преобразование, чтобы он поместился в элементе управления. При этом масштабируются все штрихи. Для примера попытаемcя создать рамку для прямоугольника, пользуясь экземпляром DrawingBrush: <Rectangle> <Rectangle.Fill> <DrawingBrush> <DrawingBrush.Drawing> <GeometryDrawing Brush=’sc# 1,.8,.8,.8’> <GeometryDrawing.Pen> <Pen Brush=’Black’ Thickness=’1’ /> </GeometryDrawing.Pen> <GeometryDrawing.Geometry> <RectangleGeometry Rect=’0,0,10,10’ /> </GeometryDrawing.Geometry> </GeometryDrawing> </DrawingBrush.Drawing> </DrawingBrush> </Rectangle.Fill> </Rectangle> 236 Глава 5. Визуальные элементы Рис. 5.20. Применение DrawingBrush для рисования границы На рис. 5.20 видно, что, хотя в объявлении указана толщина пера 1 пиксель, реально она составляет одну десятую размера прямоугольника, поскольку кисть геометрически масштабируется так, чтобы заполнить закрашиваемую область: <Rectangle Stroke=’Black’ StrokeThickness=’1’ Fill=’sc# 1,.8,.8,.8’> </Rectangle> Рис. 5.21. Использование встроенной в класс Rectangle поддержки штрихов По умолчанию элементы управления обычно не масштабируются, поэтому фигуры спроектированы так, чтобы соответствующий геометрический примитив растягивался, а не просто масштабировался. Такое растяжение допускает негео метрические преобразования (это для читателей, хорошо знакомых с предметом). На рис. 5.21 ширина штриха составляет 1 пиксель вне зависимости от размера прямоугольника. Разница между обеими моделями проявляется гораздо нагляд нее, когда мы имеем дело с путями. В этом случае свойство Stretch управляет спо собом растяжения пути при изменении размера фигуры. Единственная фигура, которая не поддерживает свойства Stretch, – это Ellipse, поскольку его поведение при изменении размера фиксировано. Двухмерная графика 237 Изображения Если приложение хоть скольконибудь значимо, в нем, вероятно, есть изобра жения. Но прежде чем двигаться дальше, надо определить, что такое изображе ние. Принято считать изображением графические данные в векторном или раст ровом формате, предназначенные для отрисовки. Мы уже много говорили о век торной графике: рисунках, фигурах, перьях, кистях. Все это необходимо для оп ределения векторных данных. В терминологии WPF изображением называется любой класс, производ ный от ImageSource. Отличительными особенностями изображения являются наличие размера и необязательных метаданных. В WPF почти все посвящено растровым изображениям, в первой версии нет формального понятия формата файла, содержащего векторное изображение. Однако класс DrawingImage поз воляет использовать в качестве изображения любой рисунок, а это уже начало полноценной векторной модели. Но мы пока сосредоточимся на растровых изображениях. Основы изображений Для начала рассмотрим два простейших и наиболее распространенных спосо ба доступа к растровым изображениям. Класс Image представляет фигуру, инкап сулирующую объект ImageSource, а класс BitmapImage – это самый употреби тельный подкласс ImageSource для доступа к растровым изображениям. <!— включение изображения в разметку —> <Image Source=’someimage.jpg’ /> // Код на языке C# для показа изображения Image img = new Image(); img.Source = new BitmapImage(new Uri(«someimage.jpg»)); Класс Image – это не более чем средство просмотра объекта ImageSource; вся суть системы работы с изображениями кроется в различных подтипах ImageSource. С любым изображением ассоциирован естественный размер; для растровых изображений это обычно число пикселей по каждому измерению, по деленная на разрешающую способность устройства (число точек на дюйм), с по мощью которого изображение было получено. Когда объект Image выводит изоб ражение, он должен както масштабировать его под размеры области просмотра, здесьто и приходит на помощь свойство Stretch. Для векторных фигур (о которых шла речь в предыдущем разделе) свойство Stretch управляет тем, как фигура масштабирует составляющие ее векторы; то же самое относится и к классу Image, инкапсулирующему свое содержимое. У свой ства Stretch есть два ключевых аспекта: направление (хотим ли мы сделать изоб ражение больше, меньше или допустимо то и другое?) и тип. Как правило, нап равление игнорируется, а вот тип представляет интерес. Есть четыре возможных значения типа (рис. 5.22): 238 Глава 5. Визуальные элементы 1. None. Изображение масштабируется до естественного размера и в области просмотра видно столько, сколько поместилось. 2. Fill. Изображение масштабируется так, чтобы занять все доступное прост ранство. 3. Uniform. Изображение занимает всю ширину или высоту доступной облас ти, при этом пропорции сторон сохраняются. 4. UniformToFill. Изображение занимает все доступное пространство, про порции сторон сохраняются, видно столько, сколько поместилось. Конвейер из объектов ImageSource Основная задача элемента управления Image состоит в том, чтобы модифици ровать внешнее представление изображения. Для этого служат свойства Stretch, RenderTransform, LayoutTransform, BitmapEffect (о нем чуть ниже) и ряд других. А что, если нам нужно изменить исходное изобажение? Рис. 5.22. Растяжение изображения с помощью элемента управления Image. Размер самого элемента во всех случаях один и тот же Предположим, что у нас есть очень большое изображение (скажем, фотогра фия, отснятая 12мегапиксельной цифровой камерой), а мы хотим показать лишь небольшую ее часть. Если взять элемент Image и указать файл в качестве источ ника данных, то изображение будет загружено в память целиком, а затем масшта бировано перед выводом на экран. Это не слишком эффективно11. 11 Если быть точным, неэффективность – не вина бедного элемента Image. Поскольку менеджер раз мещения может в любой момент насильно изменить размер изображения, то не существует спосо ба определить «правильный» размер при декодировании. Можно было бы, конечно, перегружать все изображение при попытке увеличить размер, но это привело бы к задержкам и скачкообразным изменениям. Поэтому мы решили добавить в класс BitmapImage свойства DecodePixelWidth и DecodePixelHeight, чтобы разработчики приложений могли выполнять разумную оптимизацию. Двухмерная графика 239 Из нескольких объектов ImageSource можно построить конвейер команд, вы полняемых над изображением при его декодировании. Для некоторых форматов такой подход позволяет резко повысить эффективность (например, за счет счи тывания другой разрешающей способности из файла); в других случаях в кон вейер можно просто вставить механизм кэширования. Все растровые изображения в WPF могут состоять из одного или нескольких 12 кадров (frames) . В форматах TIFF и GIF поддерживается наличие нескольких кадров в одном файле, а форматы PNG и BMP допускают только один кадр. Для загрузки изображения применяется статический метод Create класса BitmapFrame: BitmapFrame frame = BitmapFrame.Create(new Uri(«testimage.png»)); Пусть теперь мы хотим вырезать часть картинки. Можно воспользоваться классом CroppedBitmap13 и передать ему кадр frame, установив свойство Source14: CroppedBitmap crop = new CroppedBitmap(); crop.BeginInit(); crop.Source = frame; crop.SourceRect = new Int32Rect(100, 150, 400, 250); crop.EndInit(); А если нужно сделать изображение чернобелым (содержащим только два эти цвета, а не градации серого), то пригодится объект FormatConvertedBitmap, для которого снова устанавливается свойство Source: FormatConvertedBitmap color = new FormatConvertedBitmap(); color.BeginInit(); color.Source = crop; color.DestinationFormat = PixelFormats.BlackWhite; color.EndInit(); И последний шаг – вывести изображение. Поскольку классы всех объектов в конвейере (BitmapFrame, CroppedBitmap и FormatConvertedBitmap) являются производными от ImageSource, мы можем воспользоваться любым из них в каче стве источника данных для элемента Image. На рис. 5.23 показано, как выглядит окончательный результат: Не путать с фреймами HTML, кадрами видео или еще с чемнибудь одноименным. Каждый кадр в модели растрового изображения – это просто отдельное изображение. Вообщето, аналогия с кадрами видео налицо, поскольку логически видео – это просто множество раст ровых изображений. 13 Размеры прямоугольника, присваиваемого свойству SourceRect, зависят от изображения. В данном случае оригинальная фотография имела размеры 800x533 пикселей. 14 Методы BeginInit и EndInit – часть интерфейса ISupportInitialize, определенного в .NET 1.0. Обычно вызывать эти методы необязательно, но в системе работы с изображениями в WPF это необходимо. 12 Глава 5. Визуальные элементы 240 Рис. 5.23. Результирующее изображение после обработки конвейером Image img =new Image(); img.Source = color; Window w = new Window(); w.Content = img; w.Title = «Image Pipeline»; w.Show(); Обычно мы не пользуемся конвейером во всей его полноте, а ограничиваемся классом BitmapImage, производным от ImageSource, который инкапсулирует на иболее употребительные функции обработки изображений (декодирование раз мера, поворот и т.д.). Метаданные изображения Большинство современных графических форматов позволяют ассоциировать с данными еще и метаданные. Самый типичный случай – включение разного ро да информации о фотографии, отснятой цифровой камерой (рис. 5.24). У любого класса, производного от ImageSource, есть свойство Metadata, поз воляющее добраться до этой информации. Для всех растровых изображений зна чением этого свойства является объект типа BitmapMetadata. У метаданных есть две стороны: упрощенное представление хорошо известных свойств (раскрывае мых непосредственно как свойства BitmapMetadata, например, CameraModel) и API для выполнения запроса (GetQuery) на получение любой информации из хранилища метаданных. Двухмерная графика 241 Рис. 5.24. Диалоговое окно свойств в Windows Vista, в котором представлены метаданные изображения С помощью объекта BitmapFrame (в текущих сборках BitmapImage не возвра щает метаданных) можно без особого труда проинспектировать метаданные. Пе ребрав их все, мы сможем построить список метаданных, поддерживаемых дан ным объектом (непонятно почему проектировщик API решил в этом случае реа лизовать метаданные в виде набора, а не свойства): DumpMetadata(frame.Metadata); void DumpMetadata(object v) { BitmapMetadata md = v as BitmapMetadata; if (md != null) { foreach (string name in md) { Debug.WriteLine(name); Debug.IndentLevel++; DumpMetadata(md.GetQuery(name)); Debug.IndentLevel—; } } else { Debug.WriteLine(«value: « + v.ToString()); } } Эта программа выводит следующую информацию: 242 Глава 5. Визуальные элементы /app1 /{uint=0} /{uint=271} value: Canon /{uint=272} value: Canon EOS D30 /{uint=274} value: 1 ... /{uint=1} /{} value: System.Windows.Media.Imaging.BitmapMetadataBlob /{uint=259} value: 6 ... Разумеется, число печатаемых секций зависит от изображения. Загадочность имен, описывающих производителя камеры (/app1/{uint=0}/{uint=271}), – дос таточно убедительная причина пользоваться более простым API, предоставляе мым классом BitmapMetdata — CameraManufacturer. Ко всему прочему, в разных форматах эта информация кодируется поразному, а класс BitmapMetdata реали зует единую абстракцию, не зависящую от формата. Рис. 5.25. Вставка визуального элемента в изображение и вывод последнего (справа) Создание изображений Обычно источником изображений служит цифровая камера или графический редактор, но иногда приходится создавать их динамически. Это бывает полезно по двум причинам: (1) для повышения производительности (единожды сгенери ровать сложное изображение с помощью векторов и различных эффектов, а не показывать всякий раз растр) или (2) для публикации (так, все изображения в этой книге были сгенерированы с помощью WPFприложений). Для генерации растрового изображения можно воспользоваться одним из классов RenderTargetBitmap или WritableBitmap. Класс RenderTargetBitmap Двухмерная графика 243 позволяет вывести любой визуальный элемент в растровое изображение фикси рованного размера. Создав растр, мы можем либо сохранить его в файле, приме нив один из многочисленных кодировщиков изображений, или вывести на экран (класс RenderTargetBitmap также наследует ImageSource). С другой стороны, класс WritableBitmap позволяет редактировать пиксели в самом изображении. Давайте определим набор элементов: поле обогащенного текста, кнопку слева и изображение справа. При нажатии кнопки мы создадим из текстового поля изоб ражение и выведем его на экран. Интересно будет сравнить это упражнение с тем, что мы раньше делали с помощью кисти VisualBrush. Напомним, что VisualBrush – «живая» копия части пользовательского интерфейса, тогда как на этот раз мы создаем статический снимок части экрана в виде набора пикселей (рис. 5.25): <!— CreateImages.xaml —> <Window x:Class=’EssentialWPF.CreateImages’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’Create Images’ > <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width=’*’ /> <ColumnDefinition Width=’*’ /> </Grid.ColumnDefinitions> <DockPanel> <Button DockPanel.Dock=’Bottom’ Click=’Copy’>Copy</Button> <RichTextBox FontSize=’24pt’ Name=’_textBox’ /> </DockPanel> <Image Grid.Column=’1’ Name=’_display’ /> </Grid> </Window> // CreateImages.xaml.cs void Copy(object sender, RoutedEventArgs e) { RenderTargetBitmap bmp = new RenderTargetBitmap( (int)_textBox.ActualWidth, // размеры (int)_textBox.ActualHeight, 96, 96, // разрешение PixelFormats.Pbgra32); // 32 бита, Alpha+RGB // Вывести _textBox в это изображение. bmp.Render(_textBox); _display.Source = bmp; } При желании извлечь это изображение и сохранить его мы можем воспользо ваться кодировщиком. В данном случае закодируем изображение в формате JPEG. Отметим, что изображения необходимо добавлять в виде кадров: Глава 5. Визуальные элементы 244 void Copy(object sender, RoutedEventArgs e) { RenderTargetBitmap bmp = new RenderTargetBitmap( (int)_textBox.ActualWidth, // ðàçìåðû (int)_textBox.ActualHeight, 96, 96, // ðàçðåøåíèå PixelFormats.Pbgra32); // 32 áèòà, Alpha+RGB // Âûâåñòè _textBox â ýòî èçîáðàæåíèå. bmp.Render(_textBox); _display.Source = bmp; JpegBitmapEncoder encoder = new JpegBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(bmp)); using (FileStream o = File.Open(@»c:\out.jpg», FileMode.Create)) { encoder.Save(o); } } Прозрачность Обеспечить прозрачность можно разными способами; для некоторых форма тов (например, PNG) альфаканал поддерживается изначально. В WPF альфа компонента является составной частью любого цвета, а каждый визуальный эле мент имеет свойства Opacity и OpacityMask. Эти четыре механизма можно даже комбинировать, так что получается впечатляющее разнообразие путей созданий эффектов, связанных с прозрачностью. Но стоит заранее предупредить: при любом добавлении прозрачности объем выполняемой системой работы резко увеличивается. Лучше, если это делается аппаратно самой видеокартой, но даже в этом случае не обходится без накладных расходов. Если же сложный эффект должен реализовываться на программном уровне, то это приведет к падению производительности. Поскольку выполнение композиции с полупрозрачностью требует считывания ранее отрисованной час ти изображения, придется задействовать для этой цели значительную долю об щего времени работы приложения. Это не фатально, но все же не стоит приме нять прозрачность к месту и не к месту. И не забывайте измерять влияние на про изводительность. Во всех примерах из этого раздела для родительского элемента сетки будет за дан фон, так чтобы эффект прозрачности был отчетливо виден. С помощью аль факанала цвета мы можем создавать кисти, позволяющие видеть фон сквозь на ложенное изображение. В WPF все цвета поддерживают альфаканал; в станда ртной шестнадцатеричной нотации RGB первые два знака обозначают альфа компоненту. На рис. 5.26 прямоугольник залит полупрозрачным белым цветом; так как первые два символы равны AA, то альфакомпонента равна 170 (из 255), то есть коэффициент прозрачности составляет 66 процентов: <Rectangle Width=’100’ Height=’100’ Fill=’#AAFFFFFF’ /> Двухмерная графика 245 Рис. 5.26. Прямоугольник, залитый полупрозрачным белым цветом, на клетчатом фоне Рис. 5.27. Прямоугольник, закрашенный градиентной кистью, для которой у каждой опорной точки есть альфакомпонента При обсуждении цветовой модели в WPF мы уже говорили, что для любо го цвета можно задать альфаканал, в частности, и для градиентной кисти (рис. 5.27): <Rectangle Width=’100’ Height=’100’> <Rectangle.Fill> <LinearGradientBrush EndPoint=’0,1’> <LinearGradientBrush.GradientStops> <GradientStop Offset=’0’ Color=’#FFFF0000’ /> <GradientStop Offset=’.33’ Color=’#9900FF00’ /> <GradientStop Offset=’.66’ Color=’#FF0000FF’ /> <GradientStop Offset=’.9’ Color=’#00FFFFFF’ /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> Безусловно, самое выдающееся средство, связанное с прозрачностью, – это свой ство OpacityMask. Маска применяется к дереву визуальных элементов. Любую кисть с заданным альфаканалом можно использовать для рисования набора элементов. Проще всего понять это на примере. Возьмем в качестве маски прозрачности объект RadialGradientBrush. На рис. 5.28 видно, что так заданная прозрачность действует для всего содержимого панели Canvas, включая объекты Rectangle и кнопку: 246 Глава 5. Визуальные элементы Рис. 5.28. Несколько фигур, нарисованных с маской прозрачности, примененной к объемлющему контейнеру <Canvas Width=’100’ Height=’100’ Background=’Orange’> <Canvas.OpacityMask> <RadialGradientBrush GradientOrigin=’.5,.3’> <RadialGradientBrush.GradientStops> <GradientStop Offset=’0’ Color=’#FF000000’ /> <GradientStop Offset=’.33’ Color=’#00000000’ /> <GradientStop Offset=’.66’ Color=’#FF000000’ /> <GradientStop Offset=’1’ Color=’#22000000’ /> </RadialGradientBrush.GradientStops> </RadialGradientBrush > </Canvas.OpacityMask> <Rectangle Canvas.Top=’0’ Canvas.Left=’0’ Width=’50’ Height=’50’ Fill=’Red’ /> <Rectangle Canvas.Top=’50’ Canvas.Left=’50’ Width=’50’ Height=’50’ Fill=’Blue’ /> <Button Canvas.Top=’35’ Canvas.Left=’15’>Hello World</Button> </Canvas> Альфаканал кисти используется для определения альфакомпоненты каждого пикселя визуального элемента и его потомков. Внутри элемента можно применять любые средства создания изображения (в том числе цвета с альфакомпонентами, вложенные маски прозрачности и т.д.). Маска прозрач ности не влияет на интерактивность или анимацию элементов, к которым применяется. В качестве маски прозрачности годится любая кисть с альфака налом. Не только градиентные кисти, но и ImageBrush, DrawingBrush и VisualBrush могут давать весьма интересные эффекты. Вот пример использо вания ImageBrush: <Canvas Width=’100’ Height=’100’ Background=’Orange’> <Canvas.OpacityMask> <ImageBrush ImageSource=’c:\mask.png’ /> </Canvas.OpacityMask> <Rectangle Canvas.Top=’0’ Canvas.Left=’0’ Width=’50’ Height=’50’ Fill=’Red’ /> <Rectangle Canvas.Top=’50’ Canvas.Left=’50’ Width=’50’ Height=’50’ Fill=’Blue’ /> <Button Canvas.Top=’35’ Canvas.Left=’15’>Hello World</Button> </Canvas> Трехмерная графика 247 Рис. 5.29. Несколько фигур с маской прозрачности, заданной кистью ImageBrush Рис. 5.30. Изображение, использованное в качестве маски прозрачности на рис. 5.29 На рис. 5.29 показано, как с помощью разметки можно создать интересный эф фект «барельефа». Напомним, что для применения этой техники необходимо, чтобы изображение содержало альфаканал, а не просто черные и белые пиксели. Взятое в качестве маски изображение приведено на рис. 5.30. Свойство BitmapEffects Свойство OpacityMask – это первый пример механизма модификации пиксе лей, порождаемых ядром системы композиции. Возможность выполнять попик сельные операции над результатом рисования визуальных элементов обычно поддерживается свойством BitmapEffect класса UIElement. Эти эффекты называ ются растровыми, так как операция применяется к растру (пикселям), вырабо танным ядром композиции. Некоторые эффекты могут реализовываться аппа ратно за счет поддержки пиксельного наложения теней (pixel shader) в современ ных видеокартах15. Но в текущей версии применение всех растровых эффектов реализовано программно. Это заметно снижает производительность, так что пользуйтесь с осторожностью. 15 Растровые эффекты можно реализовывать в управляемом и неуправляемом коде. Однако в настоящее время мы не умеем создавать новые эффекты, исполняемые непосредственно графической аппаратурой (быть может, научимся в следующей версии WPF). Глава 5. Визуальные элементы Для элементов Для изображений 248 Рис. 5.31. Применение различных эффектов BitmapEffect к изображениям и элементам управления Хотя все эффекты применимы к любому элементу, одни (например, эффект отбрасываемой тени DropShadowBitmapEffect) лучше подходят для векторной графики, а другие (например, эффект гравировки EmbossBitmapEffect) – для растровой. На рис. 5.31 приведены примеры некоторых эффектов для растровых изображений и векторных элементов управления. Трехмерная графика Хочу начать этот раздел с признания: я не занимаюсь программированием трехмерной графики. Мне кажется, что это «круто», и я предпринимал немало попыток освоиться с этой областью, но она мне никак не дается. Однако я не ду маю, что надо так уж глубоко разбираться в 3Dграфике, чтобы применять ее в собственных программах. В WPF мы считали, что трехмерная графика должна быть согласована с двумерной. Мы попытались свести к минимуму количество новых концепций и упростить интеграцию обоих миров. В WPF трехмерные изображения – это частный случай векторной графики. Нет ни физической модели, ни обнаружения столкновений, ни других высокоу ровневых служб, которые необходимы для написания игр или создания трехмер ной среды с полным погружением. 3Dграфика в WPF – это лишь трамплин, от которого мы хотим отталкиваться в будущем. Имея уже наличествующие базо вые блоки, мы можем создавать в WPF весьма интересные картины, но для этого требуется много работы и понимание принципов трехмерной графики. Трехмерная графика 249 В двумерном мире есть четкая граница между рисунками (понимаемыми как низ коуровневые объекты композиции) и элементами управления – объектами более вы сокого уровня, которыми пользуется прикладной программист. В 3Dграфике, реали зованной в текущей версии WPF, мы отчасти приблизились к этому разделению: Visual3D – это трехмерный аналог Visual, а Model3D – аналог Drawing. Однако нет ни каркаса, необходимого для создания элементов управления, ни фигур, средств ввода и шаблонов, ни других более продвинутых служб. Но зачаткито присутствуют. Программа «Здравствуй, мир» в трехмерной ипостаси Для того чтобы хоть чтото изобразить в трехмерном виде, нужно понимать четыре основных концепции: модель, материал, источник освещения и камера. У моделей и материалов в двумерном мире есть простые аналоги: рисунки и кисти. Базовая модель в 3Dграфике называется GeometryModel3D и представ ляет трехмерный вариант геометрического примитива; это точный эквивалент класса GeometryDrawing в двумерной графике. Определение геометрического примитива в 3Dграфике сложнее, чем в 2D; начнем с построения простейшей трехмерной фигуры, а именно треугольника: MeshGeometry3D CreateTriangle() { MeshGeometry3D mesh = new MeshGeometry3D(); mesh.Positions.Add(new Point3D(0, 0, 0)); mesh.Positions.Add(new Point3D(1, 0, 0)); mesh.Positions.Add(new Point3D(0, 1, 0)); mesh.TriangleIndices.Add(0); mesh.TriangleIndices.Add(1); mesh.TriangleIndices.Add(2); return mesh; } Здесь с помощью свойства Positions определены три вершины треугольника. Затем, пользуясь свойством TriangleIndices, мы сообщаем порядок следования вершин. На базе этого примитива мы создаем модель: GeometryModel3D model = new GeometryModel3D(); model.Geometry = CreateTriangle(); Трехмерным аналогом кисти является класс Material. Материал принимает в качестве параметра кисть, задающую цвет заливки, а также имеет ряд свойств, уп равляющих отражением света: model.Material = new DiffuseMaterial(Brushes.Red); Теперь, имея модель и материал, мы можем осветить сцену. Пожалуй, самой расп ространенной ошибкой в трехмерном моделировании является неправильное распо ложение источников освещения. Если источники освещения находятся позади трех мерного объекта, слишком далеко от него или недостаточно яркие, то мы увидим Глава 5. Визуальные элементы 250 лишь черный контур фигуры. Как и в реальном мире, существует много разных типов источников освещения. Самый простой – это точечный источник, который можно представлять себе как обособленную лампу, излучающую свет во всех направлениях: PointLight light = new PointLight(); light.Position = new Point3D(12, 12, 12); light.Color = Colors.White; light.Range = 150; light.ConstantAttenuation = 1.0; Свойство Range определяет «силу» свету (световой поток), свойства Position и Color не нуждаются в пояснениях, а ConstantAttenuation – одно из свойств, опи сывающих скорость угасания света (подробнее мы рассмотрим этот вопрос ниже). Итак, у нас есть модель, материал и источник освещения. Последний шаг – создание камеры. Камера – это устройство, которое снимает сцену и проецирует ее на двумерную поверхность. Объединив объект Camera с объектом Viewport3D, мы наконец увидим сцену. Сначала подготовим камеру: PerspectiveCamera viewer = new PerspectiveCamera(); viewer.LookDirection = new Vector3D(0, 0, 1); viewer.Position = new Point3D(0, 0, 12); viewer.FieldOfView = 45; viewer.UpDirection = new Vector3D(0, 1, 0); Свойство FieldOfView (зона обзора) определяет коэффициент увеличения (зум), UpDirection – ориентацию камеры (то есть то, какое направление будет считаться «вверх»), а LookDirection – направление, в котором «смотрит» камера. Ну и напосле док объединим все это с окном проекций (viewport) и выведем на экран (рис. 5.32). using using using using using using using using System; System.Windows; System.Windows.Controls; System.Windows.Data; System.Windows.Documents; System.Windows.Media; System.Windows.Media.Media3D; System.Windows.Shapes; namespace EssentialWPF { public class Window3D : Window { public Window3D() { Title = «3D Rocks»; Viewport3D viewport = new Viewport3D(); Model3DGroup group = new Model3DGroup(); GeometryModel3D model = new GeometryModel3D(); model.Geometry = CreateTriangle(); model.Material = new DiffuseMaterial(Brushes.Red); group.Children.Add(model); Трехмерная графика PerspectiveCamera viewer = new PerspectiveCamera(); viewer.LookDirection = new Vector3D(0, 0, 1); viewer.Position = new Point3D(0, 0, 12); viewer.FieldOfView = 45; viewer.UpDirection = new Vector3D(0, 1, 0); viewport.Camera = viewer; PointLight light = new PointLight(); light.Position = new Point3D(12, 12, 12); light.Color = Colors.White; light.Range = 150; light.ConstantAttenuation = 1.0; group.Children.Add(light); ModelVisual3D scene = new ModelVisual3D(); scene.Content = group; viewport.Children.Add(scene); Content = viewport; } MeshGeometry3D CreateTriangle() { MeshGeometry3D mesh = new MeshGeometry3D(); mesh.Positions.Add(new Point3D(0, 0, 0)); mesh.Positions.Add(new Point3D(1, 0, 0)); mesh.Positions.Add(new Point3D(0, 1, 0)); mesh.TriangleIndices.Add(0); mesh.TriangleIndices.Add(1); mesh.TriangleIndices.Add(2); return mesh; } } } Рис. 5.32. Вид простой трехмерной фигуры (треугольника) 251 Глава 5. Визуальные элементы 252 Однако! Сколько кода, чтобы вывести простой треугольник. Прежде чем пе рейти к созданию более интересных фигур, сформулируем основные принципы трехмерной графики. Принципы трехмерной графики Поскольку в текущей версии WPF поддержка трехмерной графики ограничена, будет правильно сначала познакомиться с базовыми строительными блоками и по нять, что можно надстроить над ними. Мы уже говорили, что в трехмерной графике есть четыре основных концепции: модель, материал, камера и источник освещения. Модели Все трехмерные объекты в конечном итоге составлены из треугольников. В WPF ничего, кроме треугольников, нет. Отметим, что изображение в WPF фор мируется в результаты работы трехмерного конвейера. Текст, фигуры, элементы управления, рисунки – все отрисовывается в виде набора треугольников. В слу чае двумерных конструкций декомпозиция (тесселяция в терминологии 3D) производится системой. Напротив, автор трехмерной модели должен самостоя тельно составить ее из треугольников. Набор треугольников, из которых склады вается трехмерная модель, называется сеткой (mesh). 4 уб п о ыв о с ани и е Z возрастание по оси Y начальные точки First (0,1,2) Second (3,2,1) Third (4,2,3) Fifth (5,3,1) Sixth (6,5,1) 5 2 3 0 1 6 возрастание по оси X Fourth (5,4,3) Completed Рис. 5.33. Этапы конструирования трехмерного куба Строго говоря, источники освещения – это тоже модели (а в некоторых систе мах моделями являются и камеры). Однако мы пока ограничимся классами GeometryModel3D и MeshGeometry3D, которые при создании любой модели всегда идут рука об руку. Чтобы определить объект класса MeshGeometry3D, нам нужны координаты пос ледовательности точек в трехмерном пространстве. Затем, выбирая из этой последо вательности тройки точек, мы можем определить треугольники. Материал точек описывается с помощью текстур. Чтобы понять, как все это увязано, создадим более сложный трехмерный объект: часть куба (мы рассмотрим только три видимых гра ни, а построение трех оставшихся я оставляю читателю в качестве упражнения). Трехмерная графика 253 Свойство Positions определяет последовательность точек16. У куба восемь вер шин, но, поскольку нас интересуют только три грани, то нужно лишь семь точек (рис. 5.33). <MeshGeometry3D> <MeshGeometry3D.Positions> <Point3D>0,0,0</Point3D> <Point3D>1,0,0</Point3D> <Point3D>0,1,0</Point3D> <Point3D>1,1,0</Point3D> <Point3D>0,1,1</Point3D> <Point3D>1,1,1</Point3D> <Point3D>1,0,1</Point3D> </MeshGeometry3D.Positions> </MeshGeometry3D> Эта последовательность точек не определяет никаких треугольников, пока мы не сообщим сетке порядок их обхода (см. рис. 5.33). С помощью свойства TriangleIndices мы сопоставляем треугольнику три индекса. Поскольку нужно создать три грани, понадобится по меньшей мере шесть треугольников. Сначала создадим левую нижнюю половину передней грани17: <MeshGeometry3D> <MeshGeometry3D.TriangleIndices> <sys:Int32>0</sys:Int32> <sys:Int32>1</sys:Int32> <sys:Int32>2</sys:Int32> </MeshGeometry3D.TriangleIndices> </MeshGeometry3D> Затем – правую верхнюю половину: <MeshGeometry3D> <MeshGeometry3D.TriangleIndices> <sys:Int32>3</sys:Int32> <sys:Int32>2</sys:Int32> <sys:Int32>1</sys:Int32> </MeshGeometry3D.TriangleIndices> </MeshGeometry3D> Повторим эту процедуру для оставшихся четырех треугольников. Напомним, что результат существенно зависит от ориентации камеры. В данном случае мы предполагаем, что положительное направление оси Y обозначает «верх». На рис. 5.33 показа но также, что чем больше координата Z, тем дальше объект отстоит от камеры. 17 В WPF для определения поверхности треугольника применяется правило правой руки. Чтобы понять его, согните правую руку, подняв вверх большой палец. Если провести координатные оси вдоль остальных пальцев, то большой палец определит нормаль к поверхности. Можно представить и подругому: когда вы смотрите на поверхность, расположенную прямо перед глазами, вершины треугольника должны быть упорядочены против часовой стрелки. 16 254 Глава 5. Визуальные элементы И еще нужно сопоставить точкам материал (иногда он называется текстурой). С помощью TextureCoordinate свойства мы определяем, как координаты матери ала относительно охватывающего прямоугольника соотносятся с точкой. Для каждой точки сетки мы собираемся ассоциировать двумерную точку из диапазо на от (0,0) до (0,1) с точкой на материале. Рис. 5.34. Первая попытка воспользоваться текстурой, не указав координаты на ней Напомним, что при определении кисти (например, LinearGradientBrush) мы можем задать начальную и конечную точки в системе координат, связанной с ох ватывающим прямоугольником, то есть в той же системе, в которой определяют ся свойства GradientStop.Offset. Поскольку материал должен охватывать нес колько треугольников и часто приходится выполнять его рендеринг несколько раз (как в в случае куба, имеющего шесть граней), то необходимо сообщить сис теме, каким образом сопоставить материал каждому треугольнику. Так как свой ства TriangleIndices уже отображают точки на треугольники, то остается только определить материал для каждой точки. По умолчанию для каждой точки свойству TextureCoordinate присваивается значение (0,0). Поэтому, если применить кисть LinearGradientBrush, то будет ис пользован лишь цвет в точке с нулевым смещением (рис. 5.34). Однако, если взять тот же градиент, которым мы заливали прямоугольник, то результат ока жется совсем другим (рис. 5.35): <DiffuseMaterial> <DiffuseMaterial.Brush> <LinearGradientBrush EndPoint=’1,0’> <LinearGradientBrush.GradientStops> <GradientStop Offset=’0’ Color=’White’ /> <GradientStop Offset=’1’ Color=’Black’ /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </DiffuseMaterial.Brush> </DiffuseMaterial> Трехмерная графика 255 Рис. 5.35. Тот же прямоугольник, к которому применена градиентная текстура Правильно задав для сетки свойство TextureCoordinates, мы сможем получить такой же результат (плоскости, освещение и т.д.) с помощью прямоугольника, составленного из двух треугольников: <MeshGeometry3D> <MeshGeometry3D.TextureCoordinates> <Point2D>0,0</Point2D> <Point2D>1,0</Point2D> <Point2D>0,1</Point2D> <Point2D>1,1</Point2D> </MeshGeometry3D.TextureCoordinates> </MeshGeometry3D> Для нашего куба с тремя гранями необходимо определить, как обрабатывать окончание градиента. Можно сделать так, чтобы куб закрашивался слева напра во, а конечное ребро имело сплошной финальный цвет. Поскольку типичная мо дель оказывается довольно громоздкой, существует также компактная форма за писи различных свойств. Следующая компактная разметка описывает ту же сет ку, что и в предыдущих примерах: <MeshGeometry3D TextureCoordinates=’0,0 1,0 0,1 1,1 0,0 1,1 1,1 1,1’ Positions=’0,0,0 1,0,0 0,1,0 1,1,0 0,1,1 1,1,1 1,0,1’ TriangleIndices=’0,1,2 3,2,1 4,2,3 5,4,3 5,3,1 6,5,1’ /> Теперь соберем все вместе и запишем окончательную разметку для отображе ния нашей первой трехмерной фигуры (рис. 5.36): <Viewport3D Width=’200’ Height=’200’> <Viewport3D.Camera> <PerspectiveCamera LookDirection=’.7,.8,1’ Position=’3.8,4,4’ FieldOfView=’17’ UpDirection=’0,1,0’ /> </Viewport3D.Camera> 256 Глава 5. Визуальные элементы <ModelVisual3D> <ModelVisual3D.Content> <Model3DGroup> <PointLight Position=’3.8,4,4’ Color=’White’ Range=’7’ ConstantAttenuation=’1.0’ /> <GeometryModel3D> <GeometryModel3D.Geometry> <MeshGeometry3D TextureCoordinates=’0,0 1,0 0,1 1,1 0,0 1,1 1,1 1,1’ Positions=’0,0,0 1,0,0 0,1,0 1,1,0 0,1,1 1,1,1 1,0,1’ TriangleIndices=’0,1,2 3,2,1 4,2,3 5,4,3 5,3,1 6,5,1’ /> </GeometryModel3D.Geometry> <GeometryModel3D.Material> <DiffuseMaterial> <DiffuseMaterial.Brush> <LinearGradientBrush EndPoint=’1,0’> <LinearGradientBrush.GradientStops> <GradientStop Offset=’0’ Color=’Black’ /> <GradientStop Offset=’1’ Color=’White’ /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </DiffuseMaterial.Brush> </DiffuseMaterial> </GeometryModel3D.Material> </GeometryModel3D> </Model3DGroup> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> Рис. 5.36. Куб с тремя гранями, к которому применена линейноградиентная текстура Создание сложных моделей – это и есть искусство трехмерной графики. Но так же, как понимание устройства кривых Безье, градиентов и цветов не поможет нарисовать красивую картину, так и понимание треугольников и текстур не сде лает из вас истинного виртуоза трехмерной графики. Трехмерные модели неверо ятно сложны, они состоят из сотен и тысяч (и даже сотен тысяч!) треугольников. Трехмерная графика 257 И для каждого треугольника нужно задать координаты вершин и текстуру. По нятно, что вручную ни один человек сделать это не в состоянии. Поэтому, чтобы создавать хорошие модели, надо приобрести соответствующий инструмент. Диффузный Зеркальный Излучающий Рис. 5.37. Сферы из различных материалов Материалы Материал – это сочетание кисти и поведения при освещении. Диффузный ма териал рассеивает свет, изза чего объект кажется плоским; зеркальный отража ет свет, поэтому кажется, что объект «блестит»; излучающий материал испуска ет свет. Все это показано на рис. 5.37. Из имеющихся материалов можно состав лять новые. В текущей версии WPF отсутствуют карты рельефности (bump maps), карты окружения (environment maps) и другие более сложные материалы. Наверное, самым употребительным является класс DiffuseMaterial. Обычно он сочетается с классом SpecularMaterial, что позволяет создавать частично от ражающие поверхности. Для этой цели служит класс MaterialGroup. Объект класса DiffuseMaterial принимает любую кисть. На рис. 5.38 показаны резуль таты применения нескольких кистей к сферам. Не забывайте, что годится лю бая из рассмотренных нами кистей: градиентная, линейная, визуальная – какая угодно! Рис. 5.38. НЕТ ПОДПИСИ !!! Источники освещения При первой попытке заняться трехмерной графикой все, наверное, допускают одну и ту же ошибку – забывают осветить сцену (угольночерная сцена не слиш ком хорошо различима) или располагают камеру так, что она направлена в никуда. Как и при входе в темную комнату, первое, что нужно сделать, – это включить свет. Глава 5. Визуальные элементы 258 В WPF поддерживается несколько источников освещения: PointLight, SpotLight, DirectionalLight и AmbientLight (рис. 5.39). Проще всего сравнить каждый из них с аналогом в реальном мире. PointLight аналогичен лампе на каливания без абажура, свет излучается во всех направлениях из точечного источника. SpotLight подобен фонарю, свет расходится из одной точки кону сом. DirectionalLight выглядит как бесконечно длинная линейка флуоресце нтных ламп – световая плоскость в одном направлении. Наконец, у AmbientLight нет аналога в реальном мире, свет излучается из всех точек во всех направлениях. Для правильного освещения сцены обычно требуется нес колько источников. Камеры Для проецирования трехмерной сцены на двумерную плоскость нужна только одна камера. Она моделирует глаз человека, рассматривающего сцену. Чаще все го употребляется камера типа PerspectiveCamera, которая отображает сцену «ес тественным образом» (чем дальше объект, тем меньше он кажется). Две другие камеры (MatrixCamera и OrthographicCamera) очень специфичные концепции трехмерной графики, которых мы здесь касаться не будем. Ось Рис. 5.39. Типы источников освещения Угол Рис. 5.40. Две составные части AxisAngleRotation3D: ось и угол поворота вокруг нее PerspectiveCamera работает как обычная бытовая камера. Она расположена в некоторой точке трехмерного пространства, смотрит в определенном направле нии, а у ее объектива есть конкретное фокусное расстояние. Пользуясь этим свойствами, мы можем естественно перемещать камеру по сцене. Трехмерная графика 259 Преобразования Двумерный рендеринг в WPF базируется на геометрических преобразовани ях, которые, однако, скрыты внутри менеджеров размещения и простых свойств. Тем не менее для отрисовки двумерного изображения производятся многочис ленные операции над матрицами. Напомним, что изменить положение обобщес твленного рисунка можно, лишь воспользовавшись преобразованием TranslateTransform. Поскольку трехмерная инфраструктура ограничивается ба зовой функциональностью, ничего, кроме геометрических преобразований, не предусмотрено. Параллельный перенос и гомотетия не представляют особых сложностей; по сравнению с двумерным аналогом добавляется лишь третья координата. А вот поворот уже не так прост. Есть несколько подходов к определению поворота в трехмерном пространстве, но самый простой для понимания связан со свойством AxisAngleRotation3D. Для определения поворота необходима ось вращения (век тор) и угол поворота вокруг этой оси (рис. 5.40). На рис. 5.41 показаны три кону са и результат их поворота на 90 градусов вокруг разных осей. С точки зрения ка меры ось Z направлена вниз, ось Y – вверх, а ось X – направо: Рис. 5.41. Результат поворота конуса вокруг разных осей <!— Поворот вокруг оси y —> <RotateTransform3D> <RotateTransform3D.Rotation> <AxisAngleRotation3D Axis=’0,1,0’ Angle=’90’ /> </RotateTransform3D.Rotation> </RotateTransform3D> <!— Поворот вокруг оси x —> <RotateTransform3D> <RotateTransform3D.Rotation> <AxisAngleRotation3D Axis=’1,0,0’ Angle=’90’ /> </RotateTransform3D.Rotation> </RotateTransform3D> <!— Поворот вокруг оси z —> <RotateTransform3D> Глава 5. Визуальные элементы 260 <RotateTransform3D.Rotation> <AxisAngleRotation3D Axis=’0,0,1’ Angle=’90’ /> </RotateTransform3D.Rotation> </RotateTransform3D> Документы и текст Если зрить в корень, то любая книга – не более чем набор черных значков на останках деревьев. И тем не менее ей удается захватить наше внимание на долгие часы. К тому же, когда мы ее читаем, книга как таковая исчезает. «Настоящая» книга находится у нас в голове; чтение – это процесс погружения. Что же тут происходит? В чем волшебство? Билл Хилл «Волшебство чтения»18 Текст – это самый эффективный способ представить информацию человеку. Хотя картинки и графики могут нести огромный объем информации, очень труд но передать сложные данные, вообще не прибегая к печатному слову. Почти во всех приложениях нам приходится выводить тот или иной текст. Hello World Рис. 5.42. Текстовый вариант программы «Здравствуй, мир» «Здравствуй, мир» – текстовый вариант Ранее в этой книге мы уже упоминали некоторые ключевые характеристики текста. Простейший способ вывести текст – поместить его непосредственно в элемент управления TextBlock (рис. 5.42): <TextBlock> Hello World </TextBlock> Возможно, это кажется не очень интересным, но происходит тут много всего. Напомним, что все проходит через трехмерный конвейер, поэтому для вывода этого текста необходимо прочитать шрифтовые файлы, получить описания гли фов, создать либо их растровые изображения (если шрифт мелкий), либо слож ные пути (для крупных шрифтов) и, наконец, превратить все это в трехмерные треугольники для вывода. Но даже и это все не очень интересно. Самое увлека тельное начинается на следующем этапе. В WPF есть все стандартные текстовые элементы, которые необходимы для представления обогащенного текста. WPF не поддерживает перекрывающихся 18 Размещено на сайте www.poynterextra.org/msfonts/osprey.doc. Документы и текст 261 тегов, поэтому сначала необходимо все привести к нормальной древовидной фор ме (то же правило действует в RTF и в Microsoft Word, так что вы, наверное, о нем уже знаете). <TextBlock> Hello World, <Bold>bold</Bold>, <Italic>italic</Italic>, <Underline>etc.</Underline> </TextBlock> При разработке WPF было решено не создавать специальные теги для каждой комбинации атрибутов форматирования. Иначе появились бы такие монстры, как BoldItalic. Вместо этого мы отдали предпочтение атрибутам, управляющим рендерингом шрифта. Вот как можно переписать предыдущий фрагмент с ис пользованием базового элемента Run: <TextBlock> Hello World, <Run FontWeight=’Bold’>bold</Run>, <Run FontStyle=’Italic’>italic</Run>, <Run TextDecorations=’Underline’>etc.</Run> </TextBlock> Рис. 5.43. «Здравствуй мир» на разных языках и с разным форматированием Текстовые элементы бывают двух видов: встроенные и блочные. Встроенные элементы могут переходить с одной строки на другую, блочные всегда начинают ся с новой строки и логически занимают прямоугольную область на экране. Пока все хорошо, если, конечно, вы живете в англоязычной стране. Но один из больших плюсов платформы WPF состоит в том, что она изначально строилась с учетом кодировки Unicode (включая сложные сценарии и т.д.). Кодировка Unicode – это способ представления 16битовых19 символов, она включает буквы всех алфавитов мира (есть даже символы языка планеты Клингон из сериала Star Trek). Попробуем перевести наш пример на японский язык20 (рис. 5.43): <TextBlock> иероглифы <Run FontWeight=’Bold’>bold</Run>, Первоначально Unicode проектировалась как строго 16битовая система. Однако существует множество вариантов кодирования 16битовых значений, совместимых с Unicode. Одной из самых распространенных является кодировка UTF8, в которой основные символы западных языков кодируются 8 битами. В последних спецификациях Unicode есть символы, для кодиро вания которых требуется два 16битовых значения. Правильнее говорить, что Unicode – это спецификация того, как надо представлять символы. 20 Если вам хочется набрать этот пример, установите японский пакет для Windows XP. Затем пос тавьте метод ввода (IME) для японского языка, который позволит вводить японские иерогли фы с клавиатуры. 19 262 Глава 5. Визуальные элементы <Run FontStyle=’Italic’>italic</Run>, <Run TextDecorations=’Underline’>etc.</Run> </TextBlock> Элемент TextBlock предназначен для хранения одного абзаца. Можно вклю чить режим переноса строк и расположить несколько блоков один под другим, чтобы имитировать абзацы, но, если мы действительно желаем перейти на следу ющий уровень, то нужно обратиться к элементу FlowDocument. При проектирова нии WPF мы всегда следовали принципу «плати только за то, чем пользуешься»; эта философия применяется к производительности, сложности и всему остально му. Многоуровневая модель текста позволяет обойтись простым элементом для ввода одного абзаца (меньше понятий и более высокая производительность) и воспользоваться более развитым в случае, когда нужно несколько абзацев. Переключимся на элемент управления FlowDocument и добавим второй аб зац, содержащий более интересный текст. FlowDocument принимает только блочные элементы, поэтому придется погрузить исходный текст внутрь элемента Paragraph: Рис. 5.44. «Здравствуй, мир» с несколькими абзацами <FlowDocument> <Paragraph> иероглифы, <Run FontWeight=’Bold’>bold</Run>, <Run FontStyle=’Italic’>italic</Run>, <Run TextDecorations=’Underline’>etc.</Run></Paragraph> <Paragraph>We can switch over to the <Run FontFamily=’Lucida Console’>FlowDocument</Run> control, and add a second paragraph with some more interesting text.</Paragraph> </FlowDocument> FlowDocument – не отображаемый элемент. Для вывода на экран его необхо димо поместить в какойнибудь элемент просмотра. Начнем с простого элемента FlowDocumentScrollViewer, который отображает документ в прокручиваемом контейнере (рис. 5.44). Обратите внимание, что выбранный по умолчанию шрифт изменился. Пос кольку элемент TextBlock обычно используется в пользовательских интерфей сах, тогда как FlowDocument рассчитан в большей степени на документы, то умолчания для них различны. В документах шрифты с засечками (например, Документы и текст 263 Times New Roman) более удобны для восприятия, а в пользовательском интер фейс, где обычно применяются мелкие шрифты, лучше смотрятся шрифты без засечек (типа Arial или Tahoma). Рис. 5.45. «Здравствуй, мир» внутри элемента управления FlowDocumentReader Рис. 5.46. «Здравствуй, мир» со встроенными элементами управления Напомним, что в главе 3 мы обсуждали несколько элементов для просмотра документов, обеспечивающих максимально удобное восприятие содержимого. Добавив в качестве корня дерева элемент FlowDocumentReader (рис. 5.45), мы сможем лучше управлять отображением документа: <FlowDocumentReader> <FlowDocument> <Paragraph> иероглифы, <Run FontWeight=’Bold’>bold</Run>, <Run FontStyle=’Italic’>italic</Run>, <Run TextDecorations=’Underline’>etc.</Run></Paragraph> <Paragraph>We can switch over to the <Run FontFamily=’Lucida Console’>FlowDocument</Run> control, and add a second paragraph with some more interesting text.</Paragraph> </FlowDocument> </FlowDocumentReader> Документ, погруженный в элемент Reader, можно либо прокручивать в преде лах одной страницы, либо разбить на страницы. Тема интеграции обнаруживает 264 Глава 5. Визуальные элементы ся и при отображении текста; в пользовательском интерфейсе не только могут присутствовать документы с развитым содержимым, но и они сами могут содер жать внутри себя произвольные элементы управления WPF (рис. 5.46): <FlowDocumentReader> <FlowDocument> <Paragraph> иероглифы, <Run FontWeight=’Bold’>bold</Run>, <Run FontStyle=’Italic’>italic</Run>, <Run TextDecorations=’Underline’>etc.</Run></Paragraph> <Paragraph>We can <Button>switch</Button> over to the <Run FontFamily=’Lucida Console’>FlowDocument</Run> control, and add a second paragraph with some more interesting text.</Paragraph> </FlowDocument> </FlowDocumentReader> Итак, у нас есть интегрированный документ, осталось только распечатать. Существует два способа печати документа: (1) отправить команды принтеру напрямую или (2) создать документ фиксированного формата, содержащий ту же информацию. Согласно новому формату документов XML Paper Specification (XPS), создается статическая версия документа, которую мож но распечатать и архивировать. Более того, уже есть принтеры, которые под держивают XPS на аппаратном уровне в качестве одного из форматов спу линга. Чтобы преобразовать документ в формат XPS, необходимо создать файл с по мощью OPC (Open Packaging Conventions) API. Затем можно создать в контей нере объект XpsDocument и вывести его в хранилище: using (Package containerPackage = Package.Open(@»c:\test.xps», FileMode.Create)) { using (XpsDocument xpsDoc = new XpsDocument(containerPackage)) { XpsDocumentWriter writer = XpsDocument.CreateXpsDocumentWriter(xpsDoc); writer.Write( ((IDocumentPaginatorSource)theDocument).DocumentPaginator); } } Чтобы загрузить сохраненный документ в программу просмотра, доста точно дважды щелкнуть мышью по контейнеру на диске (рис. 5.47). Обрати те внимание, что при просмотре документа кнопка перестала быть интерак тивной, так как документы в формате XPS – это лишь статические снимки, которые можно экспортировать на любую платформу, печатать и т.д; в них древовидная структура приводится к плоскому тексту и простой векторной графике. Документы и текст 265 Шрифты Как ни странно, в WPF нет класса Font. Вместо него имеется набор атрибу тов, сочетание которых можно рассматривать как определение шрифта: FontFamily, FontSize, FontStretch, FontStyle и FontWeight. Есть и много других атрибутов, которые изменяет способ рендеринга текста, например: TextDecorations добавляет подчеркивание или зачеркивание, Capitals изменяет регистр букв. В общем и целом, на внешний вид отображаемого текста влияют не менее 40 свойств. Рис. 5.47. «Здравствуй, мир», распечатанный в виде XPSдокумента, при просмотре в Internet Explorer Начнем с самого начала, со свойства FamilyTypeface. Термином typeface (гар нитура) обозначается конкретный набор определений символов, чаще всего их на чертание. Гарнитуры обычно хранятся в файле типа TrueType или OpenType (OpenType – это единый формат файла, в котором могут храниться гарнитуры, представленные в разных форматах). В большинстве компьютеров под управле нием ОС Windows файлы, которые находятся в каталоге c:\windows\fonts (может изменяться в зависимости от того, куда установлена операционная система), – это гарнитуры21. 21 Гарнитура – это, по существу, программа для рендеринга текста. Как ни странно это звучит, но шрифты в чемто очень похожи на промежуточный язык (IL) CLR. Каждая гарнитура предс тавляет собой специально адаптированный вариант общего шрифта, выполненный в опреде ленном стиле. 266 Глава 5. Визуальные элементы Гарнитуры группируются в семейства, представляемые в WPF типом FontFamily. Так, Arial – это семейство шрифтов, которое (на моей машине) сос тоит из четырех гарнитур: normal (обычный), bold (полужирный), italic (курсив) и bold italic (полужирный курсив). Конструктор объекта FontFamily принимает любое число разделенных запятыми значений, так что мы можем задать имена всех семейств шрифтов22, на которые согласны (это позволяет выбрать запасной шрифт, если наиболее подходящего не окажется). Если опросить свойство FamilyNames уже сконструированного объекта FontFamily, то мы узнаем, какие семейства шрифтов были найдены. Имена семейств индексируются идентифика тором культуры, поэтому поставщики шрифтов могут предлагать локализован ные названия шрифтов: XmlLanguage lang = XmlLanguage.GetLanguage(CultureInfo.CurrentUICulture.IetfLanguageTag); FontFamily attempt1 = new FontFamily(«Arial»); string name1 = attempt1.FamilyNames[lang]; FontFamily attempt2 = new FontFamily(«ChrisFont, Tahoma»); string name2 = attempt2.FamilyNames[lang]; Обычно при запуске этой программы переменная name1 будет содержать строку «Arial», а name2 – строку «Tahoma» (если только на вашей машине не ока жется шрифта с именем ChrisFont). Зная семейство шрифтов, система автоматически подберет гарнитуру, осно вываясь на других свойствах шрифта. Если нужная гарнитура отсутствует (нап ример, иногда шрифты поставляются только с гарнитурой обычной толщины), то система сама сгенерирует требуемые глифы. Обычно качество сгенерированных гарнитур ниже, поскольку для их создания применяется обобщенный алгоритм, не учитывающий специфических особенностей. В целом работа со шрифтами в WPF несколько неуклюжа (многие детали внутреннего механизма видны снаружи), но мы пытались упростить ее настолько, насколько возможно. Возьмем, к примеру, свойство FontWeight. Для управления плотностью шрифта разработчики привыкли пользоваться свойством IsBold, но ведь на самом деле плотность – это целое число от 0 до 900. Значению «Bold» обычно соответствует число 700. Свойству FontWeight можно присвоить любое значение из допустимого диапазона, но для простоты в WPF имеется типобертка FontWeights, который содержит набор хорошо известных символьных значений. Размещение текста Выбрав и сконфигурировав шрифт, мы можем обратиться к позиционирова нию текста на странице или на экране. Располагать текст в одной строке, конеч но, небезынтересно, но рано или поздно строку приходится разбивать на части, и тут мы подходим к теме форматирования абзаца. 22 В языке XAML все имена семейств перечисляются одинаково: <Button FontFamily=’Arial, Tahoma’/>. Документы и текст 267 Абзацы Рис. 5.48. Ящичная модель размещения блочного элемента Как и все блочные элементы, абзацы поддерживают ящичную модель (box model). У каждого блочного элемента есть свойства Margin (поле), Padding (отс туп) и BorderThickness (толщина границы), которые управляют размером эле мента. На рис. 5.48 эта модель представлена графически. Отметим, что поля пер вого и второго абзаца схлопываются, то есть вместо суммы остается большее из двух полей. Также отметим, что поле второго абзаца (последнего в документе) исключено. Иными словами, хотя для обоих параграфов определены верхнее и нижнее поле, между ними оставлено только одно. Помимо ящичной модели, класс Paragraph поддерживает все стандартные средства форматирования строк: свойство TextIndent определяет отступ пер вой строки (показано на рис. 5.48), LineHeight – расстояние между строками и т.д. Кроме того, в классе Paragraph имеются свойства, необходимые для разбиения текста на страницы: KeepTogether, KeepWithNext, MinOrphanLines и MinWidowLines; все они управляют тем, что делать с со седними абзацами при переходе через границу страницы. На рис. 5.49 пока зано действие свойства KeepWithNext в случае, когда документ просматри вается в страничном режиме; заголовок «Heading 2» мог бы поместиться во второй колонке, но поскольку KeepWithNext равно True, он помещен в третью колонку. Списки Помимо обычных абзацев, бывают еще нужны списки. List – это блочный эле мент, содержащий объекты типа ListItem, которые сами являются блочными кон тейнерами. Отметим, что ListItem может содержать только блочные элементы, а Глава 5. Визуальные элементы 268 не просто текст, то есть обычно внутри ListItem находится один или несколько объектов Paragraph. Рис. 5.49. Свойство KeepWithNext предотвращает разрыв страницы или колонки сразу после заголовка Ниже приведен список из нескольких элементов, содержащий в том числе вложенный список. Свойство MarkerStyle определяет внешний вид маркера, ко торый предшествует каждому элементу списка (рис. 5.50), а StartIndex управля ет нумерацией элементов (в текущей версии WPF создание нестандартных мар керов не поддерживается). Это простой список Элемент 1 Элемент 2 Подэлемент 1 Подэлемент 2 В списках могут быть разные маркеры 1. Элемент 1 2. Элемент 2 Подэлемент 1 Подэлемент 2 Рис. 5.50. Два списка с разными маркерами <FlowDocument> <Paragraph>Это простой список:</Paragraph> Документы и текст 269 <List> <ListItem><Paragraph>Элемент 1</Paragraph></ListItem> <ListItem> <Paragraph>Элемент 2</Paragraph> <List Margin=’0’> <ListItem><Paragraph>Подэлемент 1</Paragraph></ListItem> <ListItem><Paragraph>Подэлемент 2</Paragraph></ListItem> </List> </ListItem> </List> <Paragraph BreakColumnBefore=’True’>В списках могут быть разные маркеры: </Paragraph> <List MarkerStyle=’Decimal’> <ListItem><Paragraph>Элемент 1</Paragraph></ListItem> <ListItem> <Paragraph>Элемент 2</Paragraph> <List Margin=’0’ MarkerStyle=’Box’> <ListItem><Paragraph>Подэлемент 1</Paragraph></ListItem> <ListItem><Paragraph>Подэлемент 2</Paragraph></ListItem> </List> </ListItem> </List> </FlowDocument> Таблицы Списки и абзацы – это основные строительные блоки механизма размещения текста, но прародителем всех способов размещения является элемент Table. Таб личная верстка много лет была опорой и надежей Webсообщества, поскольку, если не считать плавающего текста, это был единственный способ управления размещением. В современных версиях HTML акцент постепенно смещается на элементы DIV с полями и абсолютным позиционированием, но таблицы все еще применяются для сложной и выразительной верстки. В WPF для табличной верстки применяется элемент Table, который ведет се бя так, как мы привыкли, работая с HTML, Word и большинством программ ти пографской верстки. Так же, как и в случае List, объектная модель накладывает ряд ограничений, а именно: Table может содержать только объекты TableRowGroup, TableRowGroup – только объекты TableRow, TableRow – толь ко объекты TableCell, а TableCell – это блочный контейнер, поэтому обычно он содержит один или несколько абзацев. Поскольку классы Table, TableRowGroup, TableRow и TableCell наследуют BlockElement, то все они поддерживают ящичную модель (подобно классу Paragraph, обладают свойствами Margin, Padding и BorderThickness), что обеспе чивает высокую гибкость размещения. Кроме того, объект Table содержит набор объектов типа TableColumn, а это позволяет применять форматирование сразу ко всему столбцу. Глава 5. Визуальные элементы 270 Простая таблица Ячейка Ячейка Ячейка Ячейка Ячейка Ячейка Ячейка Ячейка Сложная таблица Ячейка Ячейка Ячейка Ячейка Ячейка Ячейка Ячейка Ячейка Рис. 5.51. Две таблицы с толстой внешней границей и тонкими границами отдельных ячеек На рис. 5.51 изображены две таблицы. Структура первой совсем проста, а на примере второй демонстрируются более развитые возможности: свойства RowSpan, ColumnSpan и вложенная таблица: <FlowDocument> <Paragraph>Простая таблица</Paragraph> <Table> <TableRowGroup> <TableRow> <TableCell><Paragraph>Ячейка 1</Paragraph></TableCell> <TableCell><Paragraph>Ячейка 2</Paragraph></TableCell> <TableCell><Paragraph>Ячейка 3</Paragraph></TableCell> <TableCell><Paragraph>Ячейка 4</Paragraph></TableCell> </TableRow> <TableRow> <TableCell><Paragraph>Ячейка 1</Paragraph></TableCell> <TableCell><Paragraph>Ячейка 2</Paragraph></TableCell> <TableCell><Paragraph>Ячейка 3</Paragraph></TableCell> <TableCell><Paragraph>Ячейка 4</Paragraph></TableCell> </TableRow> </TableRowGroup> </Table> <Paragraph>Сложная таблица</Paragraph> <Table> <TableRowGroup> <TableRow> Документы и текст 271 <TableCell ColumnSpan=’3’> <Table> <TableRowGroup> <!— другие ячейки —> </TableRowGroup> </Table> </TableCell> <TableCell RowSpan=’2’> <Paragraph>Ячейка 4</Paragraph> </TableCell> </TableRow> <TableRow> <TableCell><Paragraph>Ячейка 1</Paragraph></TableCell> <TableCell><Paragraph>Ячейка 2</Paragraph></TableCell> <TableCell><Paragraph>Ячейка 3</Paragraph></TableCell> </TableRow> </TableRowGroup> </Table> </FlowDocument> Рисунки и плавающие объекты В большинстве «настоящих» документов есть рисунки (взять, к примеру, эту кни гу – рисунков в ней больше, чем достаточно!) и плавающие объекты (floater). В совокуп ности те и другие называются заякоренными блоками. Это означает, что их положение определяется относительно какогото места в нормальном потоке текста. В разметке мы описываем это соотношение, встраивая рисунок или плавающий объект в текст. Рисунки и плавающие объекты очень похожи, но есть два отличия: рисунки могут располагаться в нескольких колонках и позиционироваться относительно того места на странице, куда помещены (с помощью свойств HorizontalAnchor и VerticalAnchor). Что касается плавающих объектов, то они, как следует из само го названия, больше похожи на плавучие бакены, сопровождающие содержимое, к которому прикреплен якорь. Те и другие иллюстрируются на рис. 5.52. Чтобы понять, как эти элементы используются, создадим в документе раздел с пла вающим объектом. Поместим изображение с подписью сразу после заголовка. Плава ющий объект – это блочный элементконтейнер (например, Section), который может содержать только блочные же элементы. Мы вставим наше изображение в элемент BlockUIContainer, позволяющий включать в документ любой элемент управления: <Paragraph FontSize=’24pt’ KeepWithNext=’True’>Heading 2</Paragraph> <Paragraph> <Floater Width=’200’> <BlockUIContainer> <StackPanel> <Image Source=’c:\testimage.png’ /> <TextBlock FontWeight=’Bold’>Image Floater</TextBlock> </StackPanel> </BlockUIContainer> </Floater> Глава 5. Визуальные элементы 272 Lorem ipsum ... </Paragraph> <Paragraph>In tincidunt ... </Paragraph> ъе к та та Плавающее изображение к ъе б об оо ег о г ще щ аю ва ю в ла ьп рь пл а ор Я ко Як Рисунок, состоящий из текста внутри абзаца Рис. 5.52. Рисунки и плавающие объекты в разбитом на страницы документе; показаны соответствующие якоря Форматирование на уровне страниц и колонок Последний уровень размещения текста – это документ в целом. В классе FlowDocument есть ряд свойств, позволяющих управлять разбиением на колонки и форматированием страниц. Основные игроки здесь – межколонные линейки (линии, разделяющие колонки) и ширина колонки, но самым интересным является свойство IsColumnWidthFlexible, которое позволяет системе автоматически вычислять размер колонок в зависимости от области, в которой отображается документ (рис. 5.53). Изменить размер Рисунок, состоящий из текста внутри абзаца Рисунок, состоящий из текста внутри абзаца Рис. 5.53. Форматирование на уровне документа, включая автоматическое вычисление размеров колонок Документы и текст 273 С помощью свойств, определяющих размер страницы документа, мы можем узнать диапазон допустимых размеров (вычисляемый исходя из минимальной и максимальной ширины и высоты страницы) и задать отступы для страницы23. Можно также зафиксировать размер страницы с помощью свойств PageWidth и PageHeight, если мы не хотим, чтобы документ поддерживал диапазон размеров. Дополнительные типографические средства Типоrрaфия – искусство представления текста, включая выбор шрифтов, размеров и способа размещения таким образом, чтобы текст был удобен для восприятия и эстетически привлекателен. Дизайнеры документов уже давно жаловались на слабую поддержку настоя щей типографии на платформе Windows. Для создания высококачественных до кументов всегда требовалось дополнительное программное обеспечение (скажем, Microsoft Word или Adobe PageMaker). Но в WPF включено совершенно новое шрифтовое ядро, а первоклассный механизм верстки страницы интегрирован в базовую платформу. Все, что мы рассматривали до сих пор (таблицы, колонки, разбиение на страницы), стало возможным именно благодаря этому ядру. Рис. 5.54. Демонстрация альтернативных форм шрифта Palatino Linotype Теперь мы можем воспользоваться заложенными в стандарт OpenType до полнительными возможностями, которые поддерживают некоторые шрифты. Например, шрифт Palatino Linotype позволяет выбирать разное начертание символов в зависимости от обстоятельств. На рис. 5.54 иллюстрируются неко торые возможности. Пожалуй, самое интересное – это дискреционные лигату ры. Лигатурой называется печатный знак, представляющий собой комбинацию двух или трёх символов. В третьем примере на рис. 5.54 соединение T и h, а так же хвостик буквы Q, продолжающийся под u, – примеры дискреционных лига тур. Большую часть возможностей стандарта OpenType предоставляет класс Typography: 23 Слово отступ в данном контексте звучит странно; обычно в этом случае говорят о полях, но мы последовательно называем внутреннее свободное пространство отступом. Глава 5. Визуальные элементы 274 <Paragraph Typography.DiscretionaryLigatures=’True’> 1..2..3.. 1/2, 1/3, The Quick brown fox... </Paragraph> Еще две дополнительных возможности – это перенос слов и выравнивание. Одна из сложностей при создании документа – соблюдение баланса белого и черного. Не выровненный край справа может приводить к появлению излишне больших пустых мест, когда встречаются длинные слова. Перенос и выравнивание (рис. 5.55), могут сгладить остроту проблемы. При переносе (когда свойство IsHyphenationEnabled равно True) используется национальный словарь для правильного разбиения слов и автоматически вставляются дефисы. При выравнивании по ширине (TextAlignment = ‘Justify’) пробелы между словами увеличиваются, так чтобы текст занял всю стро ку. Эти механизмы чаще всего применяются совместно. Рис. 5.55. Перенос и выравнивание строк определяют, как слова разбиваются по слогам и размещаются внутри абзаца Разобравшись с текстом, мы можем перейти от статических визуальных эле ментов в мир анимации и мультимедиа. Анимация Можно сказать, что анимация – это модификация некоторого значения на про тяжении времени. Любой объект, в котором периодически вычисляется некоторая функция, можно считать частным случаем анимации. Начнем с простого примера. Анимация как new Timer Чтобы лучше понять, как работает анимация, попробуем реализовать ее трудным путем. Предположим, что мы хотим увеличить размер шрифта кнопки от 9.0 до 18.0 постоянными приращениями за 5 секунд. Запустим таймер и при каждом срабатыва нии будем вычислять новое значение FontSize. Время запуска сохраним в переменной _start; тогда разность между _start и текущим временем можно будет использовать для вычисления приращения на очередной секунде (нам надо увеличить размер шрифта на 9.0 за 5 секунд). Когда будет достигнута верхняя граница, мы отключим таймер: <Window x:Class=’EssentialWPF.Animations’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’EssentialWPF’ > Анимация 275 <Button Name=’_button1’ HorizontalAlignment=’Center’ VerticalAlignment=’Center’ FontSize=’9pt’> Hello World </Button> </Window> public partial class Animations : Window { public Animations() { InitializeComponent(); DispatcherTimer timer = new DispatcherTimer(); timer.Interval = TimeSpan.FromMilliseconds(50); _start = Environment.TickCount; timer.Tick += timer_Tick; timer.IsEnabled = true; } long _start; void timer_Tick(object sender, EventArgs e) { long elapsed = Environment.TickCount _start; if (elapsed >= 5000) { _button1.FontSize = 18.0; ((DispatcherTimer)sender).IsEnabled = false; return; } _button1.FontSize = 9.0 + (9.0 / (5000.0 / elapsed)); } } Некоторые параметры в этом примере «зашиты» в код: продолжительность анима ции, начальное и конечное значения. Зашит и еще один, более тонкий аспект: частота кадров. Но прежде чем переходить к частоте кадров, явно выделим все параметры: long _start; double _duration = 5000; double _from = 9.0; double _to = 18.0; void timer_Tick(object sender, EventArgs e) { long elapsed = Environment.TickCount _start; if (elapsed >= _duration) { _button1.FontSize = _to; ((DispatcherTimer)sender).IsEnabled = false; return; } double increase = _to _from; _button1.FontSize = _from + (increase / (_duration / elapsed)); } Глава 5. Визуальные элементы 276 Теперь у нас есть универсальная функция для вычисления размера шрифта в любой момент между начальной и конечной точкой. (Поддержку уменьшения размера шрифта я оставляю читателю в качестве упражнения.) Очевидно, у этого подхода есть целый ряд недостатков (даже если исключить проблему частоты кадров). Прежде всего, всякий раз, как мы захотим создать анимируемое свойство, этот код придется писать заново. Кроме того, все кончится тем, что для каждой анимации будет создаваться свой таймер, или придется организовывать центральное хранилище информации обо всех анимациях. А теперь добро пожаловать в мир настоящей анимации. Анимация – это общий способ инкапсулировать зависимость значений свойств от времени. Отслеживание частоты кадров, обратные вызовы для вычис ления нового значения и объектная модель для создания повторно используемых анимаций – все это сосредоточено в одном месте. Мы можем переписать преды дущий пример, определив новый тип MyFontAnimation: public class MyFontAnimation : AnimationTimeline { double _from = 9.0; double _to = 18.0; public double From { get { return _from; } set { _from = value; } } public double To { get { return _to; } set { _to = value; } } public MyFontAnimation() {} public override Type TargetPropertyType { get { return typeof(double); } } public override object GetCurrentValue(object defaultOriginValue, object defaultDestinationValue, AnimationClock animationClock) { TimeSpan? current = animationClock.CurrentTime; double increase = _to _from; return _from + (increase / ((double)Duration.TimeSpan.Ticks / (double)current.Value.Ticks)); } protected override Freezable CreateInstanceCore() { return new MyFontAnimation(); } } Как видно из этого примера, для представления анимации нужно следующее: начальное значение (From), конечное значение (To), фиксированное время пере хода от начального значения к конечному (Duration) и таймер, позволяющий из менять текущее значение с течением времени (он представлен типом AnimationClock и запускается самой системой). Анимация 277 Далее мы можем модифицировать код и воспользоваться этой анимацией вместо таймера: public partial class Animations : Window { public Animations() { InitializeComponent(); MyFontAnimation animation = new MyFontAnimation(); animation.Duration = TimeSpan.FromMilliseconds(5000); _button1.BeginAnimation(Button.FontSizeProperty, animation); } } Это лишь первое знакомство с системой анимации, точнее, с классом AnimationClock. В классе MyFontAnimation определен алгоритм анимации зна чения типа double, а объект этого класса модифирует значение анимируемого свойства. Класс же AnimationClock отслеживает текущее состояние каждой вы полняющейся анимации. Методу BeginAnimation в качестве параметра передается свойство, которое мы хотим анимировать. Сделав класс MyFontAnimation производным от AnimationTimeline и наделив некоторыми параметрами, мы превратили его в универсальное средство анимации любого значения типа double. Но в WPF уже есть предопределенные анимации для всех стандартных типов данных. Поэтому последний шаг в реализации нашей собственной анимации – отказаться от нее вовсе и заменить встроенным типом DoubleAnimation: public partial class Animations : Window { public Animations() { InitializeComponent(); DoubleAnimation animation = new DoubleAnimation(); animation.From = 9.0; animation.To = 18.0; animation.Duration = TimeSpan.FromMilliseconds(5000); _button1.BeginAnimation(Button.FontSizeProperty, animation); } } Написанному нами простому коду класса MyFontAnimation недостает многих функций, имеющихся в классе DoubleAnimation. У всех встроенных в WPF анимаций имеется общий набор средств24, и одна из них – возможность начать анимацию с теку щего значения (мы не задавали свойства From). Воспользовавшись ей, мы можем из менить код, опустив значение From; при этом он сохранит полную работоспобность. 24 Возможно, изучая пространство имен System.Windows.Media, вы обратили внимание, что неко торые типы следуют общему паттерну, но не имеют общего базового типа. Для создания строго типизированных наборов, анимаций и прочего команда WPF пользовалась шаблоном генерации кода. Этот общий шаблон (в конечном продукте от него не осталось никаких следов) и гаранти рует, что у всех анимаций одни и те же свойства и методы, хотя определены они независимо. Глава 5. Визуальные элементы 278 public partial class Animations : Window { public Animations() { InitializeComponent(); DoubleAnimation animation = new DoubleAnimation(); animation.To = 18.0; animation.Duration = TimeSpan.FromMilliseconds(5000); _button1.BeginAnimation(Button.FontSizeProperty, animation); } } Познакомившись с основами работы анимации, мы можем перейти к вопросу об управлении. Вместо того чтобы начинать анимацию автоматически в момент отображения окна, сделаем так, чтобы она начиналась при наведении мыши на элемент управления. Для этого запустим анимацию не в конструкторе, а в обра ботчике события MouseEnter: public partial class Animations : Window { public Animations() { InitializeComponent(); _button1.MouseEnter += Button1_MouseEnter; } void Button1_MouseEnter(object sender, MouseEventArgs e) { DoubleAnimation animation = new DoubleAnimation(); animation.To = 18.0; animation.Duration = TimeSpan.FromMilliseconds(5000); _button1.BeginAnimation(Button.FontSizeProperty, animation); } } Общим для всех эффектов наката является возврат к исходному состоянию, когда мышь покидает элемент управления. Можно было бы написать обратную анимацию, которая возвращает шрифт к размеру 9.0, но более элегантно было бы просто иметь анимацию «при выходе». Поскольку мы не задавали значение From для анимации «при входе», то даже при изменении начального размера шрифта в разметке все будет работать правильно. Это несомненный плюс. В рассматривае мом случае анимация поддерживает режим, когда значения From или To не зада ются. При этом предполагается, что анимация продолжается от текущего значе ния свойства до не анимированного значения: public partial class Animations : Window { public Animations() { InitializeComponent(); _button1.MouseEnter += Button1_MouseEnter; _button1.MouseLeave += Button1_MouseLeave; } Анимация 279 void Button1_MouseEnter(object sender, MouseEventArgs e) { DoubleAnimation animation = new DoubleAnimation(); animation.To = 18.0; animation.Duration = TimeSpan.FromMilliseconds(5000); _button1.BeginAnimation(Button.FontSizeProperty, animation); } void Button1_MouseLeave(object sender, MouseEventArgs e) { DoubleAnimation animation = new DoubleAnimation(); animation.Duration = TimeSpan.FromMilliseconds(5000); _button1.BeginAnimation(Button.FontSizeProperty, animation); } } Выглядит неплохо, но есть проблема. Один из принципов WPF требует отде лять определение пользовательского интерфейса от поведения. Приведенный императивный код подключения анимации гораздо изящнее, чем первоначаль ный подход, основанный на таймере, но было бы еще лучше, если бы мы могли перенести часть определения интерфейса в разметку. Это становится возможным при использовании более высокого уровня анимации, который называется «рас кадровкой» (storyboard). Термин «раскадровка» пришел из киноиндустрии. Мы заранее описываем пос ледовательность эпизодов, затем кричим «Мотор», и все снимается в заданном по рядке. В нашем примере эффект при входе описывается одной раскадровкой, а эф фект при выходе – другой. Чтобы определить экземпляр класса Storyboard, необ ходимо спланировать одну или несколько анимаций и для каждой задать объект и его анимируемое свойство: public partial class Animations : Window { Storyboard _enter; Storyboard _leave; public Animations() { InitializeComponent(); DoubleAnimation animation1 = new DoubleAnimation(); animation1.To = 18.0; animation1.Duration = TimeSpan.FromMilliseconds(5000); Storyboard.SetTargetName(animation1, _button1.Name); Storyboard.SetTargetProperty(animation1, new PropertyPath(Button.FontSizeProperty)); _enter = new Storyboard(); _enter.Children.Add(animation1); DoubleAnimation animation2 = new DoubleAnimation(); animation2.Duration = TimeSpan.FromMilliseconds(5000); Storyboard.SetTargetName(animation2, _button1.Name); Storyboard.SetTargetProperty(animation2, new PropertyPath(Button.FontSizeProperty)); _leave = new Storyboard(); _leave.Children.Add(animation2); Глава 5. Визуальные элементы 280 _button1.MouseEnter += Button1_MouseEnter; _button1.MouseLeave += Button1_MouseLeave; } void Button1_MouseEnter(object sender, MouseEventArgs e) { _enter.Begin(this); } void Button1_MouseLeave(object sender, MouseEventArgs e) { _leave.Begin(this); } } Вот теперь мы очень близки к цели. Выделив переменную, в которой хранят ся описания свойств, мы можем перенести ее в разметку. Поскольку объекты Storyboard – не элементы управления, необходимо поместить их в секцию ресур сов окна. Подробно ресурсы мы будем рассматривать в главе 6, а пока достаточно знать, что с ресурсом можно ассоциировать ключ и обращаться к нему по этому ключу с помощью метода FindResource: <Window x:Class=’EssentialWPF.Animations’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’EssentialWPF’ > <Window.Resources> <Storyboard x:Key=’_enter’> <DoubleAnimation To=’18.0’ Duration=’0:0:5’ Storyboard.TargetName=’_button1’ Storyboard.TargetProperty=’FontSize’ /> </Storyboard> <Storyboard x:Key=’_leave’> <DoubleAnimation Duration=’0:0:5’ Storyboard.TargetName=’_button1’ Storyboard.TargetProperty=’FontSize’ /> </Storyboard> </Window.Resources> <Button Name=’_button1’ HorizontalAlignment=’Center’ VerticalAlignment=’Center’ FontSize=’9pt’> Hello World </Button> </Window> Определив анимации в разметке, мы можем изменить код так, чтобы он искал нужный ресурс и активировал подходящий экземпляр Storyboard: Анимация 281 public partial class Animations : Window { public Animations() { InitializeComponent(); _button1.MouseEnter += Button1_MouseEnter; _button1.MouseLeave += Button1_MouseLeave; } void Button1_MouseEnter(object sender, MouseEventArgs e) { ((Storyboard)FindResource(«_enter»)).Begin(this); } void Button1_MouseLeave(object sender, MouseEventArgs e) { ((Storyboard)FindResource(«_leave»)).Begin(this); } } Напомним, что мы сделали. Начали мы с использования таймера и написали анимацию вручную. Затем обнаружилось, что вместо этого можно воспользо ваться готовым классом DoubleAnimation. С помощью имеющихся в нем свойств мы реализовали простой эффект наката, создав анимацию, которая работает без явного задания значений From и To. И, наконец, мы объявили эти анимации в разметке, а для управления ими применили раскадровку. Остался последний шаг. Ассоциация между событием (MouseEnter и MouseLeave) и действием (запуск Storyboard) попрежнему «зашита» в код. Ес ли мы хотим, чтобы вся последовательность отображения объявлялась в размет ке, то нужно перенести туда и этот кусочек. Для этого завершающего этапа понадобится новая концепция: триггеры. Мы еще будем говорить о них в главе 7, но сама идея настолько важна для анимации, что стоит упомянуть о ней прямо здесь. Триггеры позволяют декларативно ассо циировать действие с событием или значением свойства. В данном случае мы ассоциируем событие MouseEnter с действием BeginStoryboard. Вместо того чтобы определять раскадровку в секции ресурсов окна, мы можем описать ее прямо в теге BeginStoryboard, и она будет вызывать ся напрямую. То же самое можно сделать и для события MouseLeave: <Window x:Class=’EssentialWPF.Animations’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’EssentialWPF’ > <Button Name=’_button1’ HorizontalAlignment=’Center’ VerticalAlignment=’Center’ FontSize=’9pt’> <Button.Triggers> <EventTrigger RoutedEvent=’Button.MouseEnter’> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> 282 Глава 5. Визуальные элементы <DoubleAnimation To=’18.0’ Duration=’0:0:5’ Storyboard.TargetName=’_button1’ Storyboard.TargetProperty=’FontSize’ /> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> <EventTrigger RoutedEvent=’Button.MouseLeave’> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <DoubleAnimation Duration=’0:0:5’ Storyboard.TargetName=’_button1’ Storyboard.TargetProperty=’FontSize’ /> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </Button.Triggers> Hello World </Button> </Window> Вот теперь действительно все. Мы проделали путь от чисто императивного ре шения, основанного на таймерах, до полностью декларативного описания с по мощью встроенной системы анимирования. Мы видели, как можно расширить эту систему, не затрагивая при этом большую часть типичных операций. А теперь внимательно рассмотрим те идеи, которые лежат за всем увиденным. Время и временная шкала Первое и самое главное, что нужно понимать, когда речь идет об анимации, – это время. В реальном мире идея времени проста; оно началось давнымдавно и течет с постоянной скоростью в одном направлении. В анимации со временем все не так просто. Время при анимации всегда соотносится с некоторой временной шкалой (timeline). Точка отсчета (нулевой момент) считается началом шкалы, а «30 се кунд» всегда означает 30 секунд от начала шкалы. Временные шкалы организова ны иерархически; начало и конец каждой отсчитываются от начала родительской шкалы. На вершине иерархии находится «глобальная» шкала, начало которой по определению совпадает с моментом создания процесса. Чтобы лучше понять идею относительных временных шкал, предположим, что нужно анимировать свойство типа double, начиная с момента 0:0:2, или 2 се кунды. Такая анимация начнется спустя 2 секунды с момента запуска содержа щей ее раскадровки. Если продолжительность анимации составляет 0:0:5, то за кончится она через 7 секунд после начала раскадровки: Анимация 283 <Storyboard> <DoubleAnimation BeginTime=»0:0:2» Duration=»0:0:5» /> </Storyboard> Глобальное время Продолжительность раскадровки 0:0:7 0:0:2 Начало Анимация double Рис. 5.56. Временная иерархия; анимация значения типа double начинается через 2 секунды и продолжается 5 секунд Этот пример изображен на рис. 5.56. Продолжительность раскадровки мы не задавали, поэтому она закончится, когда мы дойдем до конца самой длинной до черней шкалы, другими словами, на седьмой секунде. В WPF не существует спо соба объявить анимацию относительно глобального времени. Время в WPF измеряется в часах, минутах, секундах и миллисекундах. Может, вам это кажется очевидным, но в системах анимирования применяется и другой способ измерения времени: по числу кадров. WPF автоматически вычисляет частоту смены кадров для каждого приложения, поэтому на одних машинах (и при определенных ус ловиях) можно получить 120 кадров в секунду, а на других – всего 12. В любом слу чае продолжительность анимации определяется реальным истекшим временем. И последнее, что нужно знать о времени, – это то, что оно изменчиво. В WPF можно изменить темп для любой временной шкалы, так чтобы время текло быст рее или медленнее. Кроме того, можно сдвигать время вперед или назад в объек те AnimationClock, который отслеживает время указанной анимации. Програм мыконструкторы для создания анимаций часто позволяют манипулировать ло кальным временем сцены, чтобы можно было подкорректировать значение того или иного свойства в конкретный момент. Определение анимации Приручив время, мы можем перейти к определению анимации. Анимация чем то напоминает градиент: можно определить начальную точку (опорную точку со смещением 0), ряд промежуточных точек (остальные опорные точки) и что должно случиться в конце. В области анимирования мы говорим о начальной точке (From), промежуточных точках (определенных с помощью ключевых кадров или свойства To) и о поведении в конце процесса (FillBehavior, RepeatBehavior, AutoReverse). Чтобы понять, как все это работает совместно, определим анимацию для изме нения размера эллипса. Нам понадобятся две анимации: одна для ширины, дру гая для высоты. Проще всего для этой цели воспользоваться анимацией типа From/To. В данном случае мы хотим анимировать эллипс с именем _target: Глава 5. Визуальные элементы 284 <Storyboard> <DoubleAnimation From=’5’ To=’45’ Duration=’0:0:12’ Storyboard.TargetName=’_target’ Storyboard.TargetProperty=’Width’ /> <DoubleAnimation From=’5’ To=’45’ Duration=’0:0:12’ Storyboard.TargetName=’_target’ Storyboard.TargetProperty=’Height’ /> </Storyboard> Эта анимация длится 12 секунд, на протяжении которых ширина и высота эллипса плавно увеличиваются от 5 до 45. Когда анимация завершится, эллипс станет больше. А теперь заставим эту анимацию продолжаться бесконечно, запустив как фоновую. Свойство RepeatBehavior объекта Storyboard применяется ко всей анимации, опреде ленной в Storyboard, поэтому дочерние анимации тоже будут повторяться бесконечно: <Storyboard RepeatBehavior=’Forever’> <DoubleAnimation From=’5’ To=’45’ Duration=’0:0:12’ Storyboard.TargetName=’_target’ Storyboard.TargetProperty=’Width’ /> <DoubleAnimation From=’5’ To=’45’ Duration=’0:0:12’ Storyboard.TargetName=’_target’ Storyboard.TargetProperty=’Height’ /> </Storyboard> Свойство RepeatBehavior определяет, как долго должна продолжаться анима ция или сколько следует выполнить итераций. Значение Forever – это особый случай, означающий, что повторять анимацию следует бесконечно. При выполне нии этой программы в конце каждой итерации происходит мгновенный переход с 45 на 5. Если задать еще и свойство AutoReverse, то объект Storyboard обратит все дочерние анимации вспять (так что на полный обратный переход потребует ся все те же 12 секунд), а затем начнет все сначала: время 0:0:0 0:0:2 0:0:4 0:0:12 Рис. 5.57. Опорные кадры для двух анимаций – высоты и ширины эллипса Анимация 285 <Storyboard AutoReverse=’True’ RepeatBehavior=’Forever’> <DoubleAnimation From=’5’ To=’45’ Duration=’0:0:12’ Storyboard.TargetName=’_target’ Storyboard.TargetProperty=’Width’ /> <DoubleAnimation From=’5’ To=’45’ Duration=’0:0:12’ Storyboard.TargetName=’_target’ Storyboard.TargetProperty=’Height’ /> </Storyboard> Теперь анимация идет плавно в обоих направлениях. Сделаем еще одну вещь: определим набор опорных кадров для анимации. Это способ задать несколько мо ментов и значения свойства, которые должны быть достигнуты к каждому из них. Вместо того чтобы выполнять анимацию с постоянной скоростью от начала до кон ца, мы можем определить несколько «минианимаций» скажем так, как показано на рис. 5.57, где ширина и высота изменяются с разной скоростью. Для этого надо вместо класса DoubleAnimation взять класс DoubleAnimationUsingKeyFrames, он то и позволит задать последовательность опорных кадров: <Storyboard AutoReverse=’True’ RepeatBehavior=’Forever’> <DoubleAnimationUsingKeyFrames Storyboard.TargetName=’_target’ Storyboard.TargetProperty=’Width’> <LinearDoubleKeyFrame KeyTime=’0:0:0’ Value=’5’ /> <LinearDoubleKeyFrame KeyTime=’0:0:2’ Value=’10’ /> <LinearDoubleKeyFrame KeyTime=’0:0:4’ Value=’15’ /> <LinearDoubleKeyFrame KeyTime=’0:0:12’ Value=’45’ /> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetName=’_target’ Storyboard.TargetProperty=’Height’> <LinearDoubleKeyFrame KeyTime=’0:0:0’ Value=’5’ /> <LinearDoubleKeyFrame KeyTime=’0:0:2’ Value=’5’ /> <LinearDoubleKeyFrame KeyTime=’0:0:4’ Value=’25’ /> <LinearDoubleKeyFrame KeyTime=’0:0:12’ Value=’45’ /> </DoubleAnimationUsingKeyFrames> </Storyboard> У системы анимирования в WPF есть и много других возможностей, не упо мянутых выше. Например, кроме LinearDoubleKeyFrame, существуют и другие типы опорных кадров. Определение анимации может быть очень сложным; как и в случае двумерной или трехмерной графики, для создания хорошей, реалистич ной анимации необходим подходящий инструмент. В примере с эллипсом в одной раскадровке определено две анимации, то есть класс Storyboard является производным не только от Timeline, но также и от TimelineGroup. Понятие композиции, которое встречалось нам при рассмотре Глава 5. Визуальные элементы 286 нии элементов управления, присутствует также и в мире анимации. С помощью композиции временных шкал мы можем вкладывать анимации друг в друга и за давать их порядок для достижения желаемого эффекта. Интеграция анимации Прежде чем покончить с анимацией, кратко остановимся на вопросе об интегра ции ее с прочими частями платформы. Можно анимировать любое свойство, име ющее значащий тип (value type), но анимация встроена и еще в несколько мест25. Интеграция с управляющими шаблонами Анимация – прекрасное средство для создания мультимедийного содержимо го, но с точки зрения разработчика, наверное, более интересна возможность включения анимации в элементы управления. Представьте себе кнопку, рамка которой меняет толщину и цвет при наведении на нее мыши (рис. 5.58). Вместо того чтобы зашивать переход в сам элемент, мы можем определить раскадровку как часть объекта ControlTemplate. Применяя уже рассмотренные идеи, мы можем записать в свойство ControlTemplate.Triggers два объекта EventTrigger. Если ассоциировать элемент BeginStoryboard с подходящей раскадровкой, то анимация будет ассоциирована с шаблонным элементом управления. Отметим также, что частям дерева отображе ния внутри ControlTemplate можно присвоить имена и обращаться по этим име нам из Storyboard: По умолчанию При наведении мыши Hello World Hello World Рис. 5.58. Применение анимации в шаблоне для создания интерактивной кнопки <Button> <Button.Template> <ControlTemplate TargetType=’{x:Type Button}’> <Border Name=’_border’ CornerRadius=’4’ BorderBrush=’Black’ BorderThickness=’1’> <ContentPresenter HorizontalAlignment=’Center’ VerticalAlignment=’Center’ /> </Border> <ControlTemplate.Triggers> <EventTrigger RoutedEvent=’Button.MouseEnter’> 25 Хотя тип анимируемого свойства может быть любым, но само свойство должно быть реализо вано с помощью DependencyProperty. Анимация 287 <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <ColorAnimation To=’Red’ Storyboard.TargetName=’_border’ Storyboard.TargetProperty= ‘BorderBrush.Color’ /> <ThicknessAnimation To=’5’ Storyboard.TargetName=’_border’ Storyboard.TargetProperty= ‘BorderThickness’ /> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> <EventTrigger RoutedEvent=’Button.MouseLeave’> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <ColorAnimation Storyboard.TargetName=’_border’ Storyboard.TargetProperty= ‘BorderBrush.Color’ /> <ThicknessAnimation Storyboard.TargetName=’_border’ Storyboard.TargetProperty= ‘BorderThickness’ /> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </ControlTemplate.Triggers> </ControlTemplate> </Button.Template> Hello World </Button> Интеграция с текстом Дальнейшие эксперименты с анимацией могут поставить нас перед любопыт ной проблемой: как сделать объектом анимации отдельные символы в сегменте текста. Изза сложности системы размещения (переносы, форма глифов и т.д.) текст невозможно размещать посимвольно, то есть нельзя просто разбить текст на элементы Span, содержащие по одному символу. WPF поддерживает посим вольную верстку с помощью специального свойства текста TextEffects. Это свой ство, имеющееся у всех текстовых элементов, позволяет разработчику или дизай неру применить некое преобразование к отдельным символам. Каждый объект типа TextEffect содержит экземпляры объектов Transform, PositionStart и Глава 5. Визуальные элементы 288 PositionCount, которые определяют, на какие символы эффект распространяется. Для демонстрации того, как анимация сочетается с текстом, напишем програм му, которая заставит произвольный текст внутри блока прыгать вверхвниз. Начнем с определения пользовательского интерфейса в разметке. Можно было бы описать в разметке и эффекты, но, поскольку мы хотим, чтобы каждый символ начинал дви жение в разные моменты времени, проще будет создать анимацию программно: <Window x:Class=’EssentialWPF.TextEffectTest’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’TextEffects’ > <TextBlock FontSize=’72pt’ Name=’_text’ > This is animated text </TextBlock> </Window> Далее нужно определиться с базовой структурой программы. Мы собираемся на каждой итерации обрабатывать один символ внутри блока, поскольку для каждого символа нам нужен свой эффект: public partial class TextEffectTest : Window { public TextEffectTest() { InitializeComponent(); for (int i = 0; i < _text.Text.Length; i++) { ... } } } Сначала построим объекты TextEffect. С каждым эффектом ассоциируем начальную позицию и счетчик символов, а также преобразование, которое будем анимировать: public partial class TextEffectTest : Window { public TextEffectTest() { InitializeComponent(); _text.TextEffects = new TextEffectCollection(); for (int i = 0; i < _text.Text.Length; i++) { TextEffect effect = new TextEffect(); effect.Transform = new TranslateTransform(); effect.PositionStart = i; effect.PositionCount = 1; _text.TextEffects.Add(effect); } } } Анимация 289 Наконец, создадим раскадровку, которая будет применять анимацию к преоб разованиям в созданных объектах TextEffect: public partial class TextEffectTest : Window { public TextEffectTest() { InitializeComponent(); Storyboard perChar = new Storyboard(); _text.TextEffects = new TextEffectCollection(); for (int i = 0; i < _text.Text.Length; i++) { TextEffect effect = new TextEffect(); effect.Transform = new TranslateTransform(); effect.PositionStart = i; effect.PositionCount = 1; _text.TextEffects.Add(effect); DoubleAnimation anim = new DoubleAnimation(); anim.To = 5; anim.AccelerationRatio = .2; anim.DecelerationRatio = .2; anim.RepeatBehavior = RepeatBehavior.Forever; anim.AutoReverse = true; anim.Duration = TimeSpan.FromSeconds(2); anim.BeginTime = TimeSpan.FromMilliseconds(250 * i); Storyboard.SetTargetProperty(anim, new PropertyPath(«TextEffects[« + i + «].Transform.Y»)); Storyboard.SetTargetName(anim, _text.Name); perChar.Children.Add(anim); } perChar.Begin(this); } } Что получилось, показано на рис. 5.59. Мультимедиа Говоря об изображениях, мы столкнулись с интересной проблемой определе ния смысла самого термина «изображение». То же самое имеет место и для муль тимедиа. Простое определение в контексте WPF гласит: «поток данных с времен ными метками». При таком определении всякая анимация технически является частным случаем мультимедиа. Но в рамках этого обсуждения мы ограничимся двумя наиболее употребительными типами внешнего медийного содержимого: аудио и видео. Аудиофайлы содержат волновые сигналы, организованные во вре мени; видеофайлы – аналогичным образом организованные растровые изображе ния. Но как эти файлы проигрываются? Глава 5. Визуальные элементы 290 Рис. 5.59. Свойство TextEffects позволяет применять анимацию к отдельным символам Аудио У аудио и видеофайлов общая реализация – класс MediaTimeline. Представле ние мультимедийного содержимого в виде временной шкалы может показаться странной идеей, но на самом деле «timeline» – это абстрактное определение анима ции, которая и составляет существо любого аудио или видеофайла. Для воспроиз ведения любого мультимедийного содержимого необходимы «часы» – MediaClock. Вспомните, как при обсуждении анимации мы проводили различие между собственно определением анимации (Timeline) и одним ее прогоном (Clock). Для воспроизведения аудиофайла нужно создать экземпляр класса MediaTimeline и в его свойство Source записать адрес источника данных: public class MediaAudio : Window { MediaTimeline _audioTimeline; public MediaAudio() { _audioTimeline = new MediaTimeline(); _audioTimeline.Source = new Uri(@»C:\...\Beethoven’s Symphony No. 9 (Scherzo).wma»); } } Временная шкала сохраняется в переменнойчлене, чтобы ее не убрал сбор щик мусора. Поскольку видео обычно отображается в составе дерева элементов, о нем тревожиться нет нужды, а вот ссылки на объекты, представляющие аудио, необходимо сохранять! Помимо временной шкалы, следует еще создать часы, которые предстоит «за вести». Для обычной анимации, кроме часов, больше ничего и не нужно, но для мультимедийных файлов требуется еще объектпроигрыватель MediaPlayer, ко торый управляет состоянием воспроизведения: public class MediaAudio : Window { MediaTimeline _audioTimeline; MediaClock _audioClock; public MediaAudio() { Мультимедиа 291 _audioTimeline = new MediaTimeline(); _audioTimeline.Source = new Uri(@»C:\...\Beethoven’s Symphony No. 9 (Scherzo).wma»); _audioClock = _audioTimeline.CreateClock(); MediaPlayer player = new MediaPlayer(); player.Clock = _audioClock; } } Подготовив прогрыватель и часы, мы можем приступить к воспроизведению музыки. Глядя на объектную модель MediaPlayer, сразу хочется вызвать метод Play, но это было бы ошибкой. Класс MediaPlayer поддерживает два режима: свя занный и несвязанный. Если объект MediaPlayer привязан к часам, то часы игра ют главенствующую роль, а проигрыватель просто наблюдает за ними. Управ лять воспроизведением следует, вызывая методы объектачасов, а не самого про игрывателя. В несвязанном режиме MediaPlayer создает часы внутри себя, и тог да им можно пользоваться напрямую (ниже мы это увидим). Чтобы хоть чтонибудь увидеть в самой книге, подпишемся на событие часов CurrentTimeInvalidated и будем обновлять свойство Window.Title, показывая, что мы «слушаем» музыку (рис. 5.60): public class MediaAudio : Window { MediaTimeline _audioTimeline; MediaClock _audioClock; public MediaAudio() { _audioTimeline = new MediaTimeline(); _audioTimeline.Source = new Uri(@»C:\Users\Public\Music\Sample Music\Symphony_No_3.wma»); _audioClock = _audioTimeline.CreateClock(); MediaPlayer player = new MediaPlayer(); player.Clock = _audioClock; _audioClock.CurrentTimeInvalidated += TimeChanged; _audioClock.Controller.Begin(); } void TimeChanged(object sender, EventArgs e) { Title = _audioClock.CurrentTime.ToString(); } } Три базовых объекта – временная шкала, часы и прогрыватель – присутству ют при работе с мультимедиа всегда. WPF скрывает эти детали за обертывающим элементом MediaElement: Глава 5. Визуальные элементы 292 <MediaElement Source=’C:\Users\Public\Music\Sample Music\Symphony_No_3.wma’ /> Рис. 5.60. Воспроизведение музыки (а вы и не подозревали, что это можно показать на картинке?) Объект MediaElement с помощью свойства Player предоставляет доступ к прогрывателю, который позволяет управлять различными аспектами воспроиз ведения. Если требуется чтото более сложное (например, подписаться на собы тие CurrentTimeInvalidated), то придется создать в программе временную шкалу и часы. Видео Воспроизвести видео можно с помощью того же объекта MediaElement, кото рый мы только что рассматривали. Поскольку MediaElement – на самом деле ви зуальный элемент (что для проигрывания музыки несколько странновато), то воспроизвести видео ему не составит труда (рис. 5.61): <Window ... Title=’Media Video’> <MediaElement Source=’C:\Windows\Help\Windows\enUS\mouse.wmv’ /> </Window> Вспомним начало этой главы, где было сказано, что одной из основ WPF яв ляется принцип интеграции. Коль скоро элемент Image – это способ визуализи ровать данные изображения, представленного в свою очередь в виде кисти, кото рой можно нарисовать любой штрих, закрасить фигуру и даже материал трехмер ного объекта, то же самое должно быть верно в отношении видео. Класс VideoDrawing позволяет включить видео в любой рисунок. Поскольку кисть DrawingBrush может заполнить заданным рисунком все, что угодно, то по чему бы не связать ее с видеофайлом и не использовать внутри обогащенного текста (рис. 5.62): <!— MediaVideo.xaml —> <Window ... Title=’Media Video’> <RichTextBox Мультимедиа 293 FontSize=’148pt’ FontFamily=’Arial’ FontWeight=’Bold’ Background=’Yellow’ Name=’_text’> <RichTextBox.Document> <FlowDocument> <Paragraph> Welcome to Video! </Paragraph> </FlowDocument> </RichTextBox.Document> </RichTextBox> </Window> // mediavideo.xaml.cs public partial class MediaVideo : Window { MediaTimeline _videoTimeline; MediaClock _videoClock; public MediaVideo() { InitializeComponent(); _videoTimeline = new MediaTimeline(); _videoTimeline.Source = new Uri(@»C:\Windows\Help\Windows\enUS\mouse.wmv»); VideoDrawing v = new VideoDrawing(); v.Player = new MediaPlayer(); v.Rect = new Rect(0, 0, 1, 1); v.Player.Clock = _videoClock = _videoTimeline.CreateClock(); _text.Foreground = new DrawingBrush(v); } } Рис. 5.61. Воспроизведение видео с помощью элемента MediaElement Глава 5. Визуальные элементы 294 Рис. 5.62. Использование видео внутри обогащенного текста Чего мы достигли? В этой главе мы познакомились с тем, как визуализируются элементы управ ления. Мы видели, как приложения создаются из элементов управления, позици онированных менеджерами размещения и отображаемых с помощью визуальных элементов. Все подсистемы WPF спроектированы для совместной работы, одна служит основой для другой. 295 Глава 6. Данные Платформа Windows Presentation Foundation отличается от многих других каркасов для создания пользовательских интерфейсов в части обработки данных. Старая модель на базе User32 о данных практически ничего не знала. Поскольку элементы управления содержат данные, то для отображения чеголибо требова лось преобразовать данные в нечто понятное элементу. В других каркасах, ска жем в библиотеке Java Swing, имеется строгая модель данных, требующая четко го разделения между элементом управления и данными. В WPF мы хотели, что бы отделение данных от ГИП было возможно, но не обязательно. Хотя WPF и позволяет смешивать данные и пользовательский интерфейс, мо дели, управляемые данными, все же обеспечивают большую гибкость и возмож ность совместной работы программистов и дизайнеров. Принципы работы с данными Как правило, приложение создается для отображения или создания тех или иных данных. Что бы ни представляли собой данные – документ, базу данных или чертеж, главная задача приложения состоит в том, чтобы их отобразить, создать и отредактировать. Способов представления данных столько же, сколь ко приложений. Уже с момента возникновения на платформе .NET существова ла стандартная модель, которая существенно изменила подходы к обработке данных. Модель данных в .NET Модель данных описывает контракт между источником и потребителем дан ных. Исторически сложилось так, что каждый каркас нес с собой новую модель данных: В Visual Basic это сначала была DAO (Data Access Objects), потом RDO (Remote Data Objects) и, наконец, ADO (ActiveX Data Objects). На платформе .NET произошел переход от APIзависимых моделей к единой для всего каркаса. В .NET имеется некая объектная модель, содержащаяся классы, интерфей сы, структуры, перечисления, делегаты и т.д. Но сверх того включена очень простая модель данных. Списки в .NET представляются интерфейсами из пространства имен System.Collections: IEnumerable и IList. Свойства определя ются либо с с помощью встроенных в CLR свойств, либо путем реализации ин терфейса ICustomTypeDescriptor. Эта базовая модель осталась неизменной, несмотря на появление новых технологий доступа к данным, и даже пережила существенные изменения в самом языке, например, появление языковонезави 296 Глава 6. Данные симых запросов, включение которых в следующую версию .NET находится в стадии обсуждения. В .NET также имеется и несколько конкретных способов работы с данными, например: ADO.NET (пространство имен System.Data), XML (пространство имен System.Xml), контракт о данных (пространство имен System.Runtime.Serialization) и разметка (пространство имен System.Windows.Markup). И это далеко не все. Са мое важное, что все они построены поверх базовой модели данных .NET. Поскольку все операции с данными в WPF базируются на фундаменте моде ли данных .NET, то элементы управления WPF могут получать данные от любо го объекта CLR: ListBox listBox1 = new ListBox(); listBox1.ItemsSource = new string[] { «Hello», «World» }; Намита Гупта (Namita Gupta), менеджер команды WPF, замечательно выра зился по поводу этой функциональности: «Ваши данные, что хотите, то и делай те». Коль скоро к данным можно получить доступ с помощью CLR, их можно и визуализировать в WPF. Всепроникающее связывание Из всех нововведений, появившихся в WPF, для программиста, пожалуй, са мым важным является полная интеграция связывания в систему. Шаблоны эле ментов, ресурсы и даже базовая «модель содержимого», лежащая в основе таких элементов управления, как Button и ListBox, – все может быть привязано к дан ным. Идея привязки свойств к источникам данным, отслеживания зависимостей и автоматического обновления экрана пронизывает все части WPF. У связывания в WPF много названий. Обычно мы выбирали новое имя, ес ли у некоей разновидности связывания имелось ограничение или поведение, специфичное лишь для конкретного сценария. Например, для привязки к шаб лонам в разметке применяется обозначение {TemplateBinding} вместо станда ртной нотации {Binding}, используемой для привязки к данным. Привязка к шаблону работает лишь в контексте некоего шаблона, и при этом допускается привязывать лишь свойства шаблонного элемента управления. Это ограниче ние обеспечивает очень высокую эффективность привязки, что абсолютно не обходимо, так как в WPF очень многие элементы управления привязываются к шаблонам. Привязка позволяет синхронизировать два представления данных. В WPF это означает, что некие данные (то есть свойство источника данных) привязыва ются к свойству элемента пользовательского интерфейса. Привязка прекрасно годится для поддержания синхронизации между двумя объектами, но, если типы данных этих объектов согласуются не идеально, начинают возникать проблемы. Чтобы идея привязки действительно проникла во все части каркаса, было необ ходимо предложить способы преобразования данных. Ресурсы 297 Преобразование данных В любом каркасе с повсеместной привязкой доступ к произвольным данным возможен лишь тогда, когда эти данные могут быть преобразованы в любом мес те, где привязка поддерживается1. WPF поддерживает два основных вида преобразований: конвертацию значе ний и шаблоны данных. Конвертеры значений просто преобразовывают данные из одного формата в другой (например, строку «Red» в Brushes.Red). Это весьма мощный механизм, поскольку конвертер может осуществлять двустороннее пре образование. Шаблоны данных, с другой стороны, позволяют «на лету» создавать элементы управления для представления данных (подобная идея реализована в языке XSLT – Extensible Stylesheet Language Transformations). В модели шабло нов данных элементы управления могут с помощью привязок обращаться к дан ным, то есть читать и потенциально даже записывать данные обратно в источник. С помощью преобразований можно получить очень выразительные представ ления данных. Мы начнем изучение данных в WPF с одного из наиболее употре бительных источников – ресурсов. Ресурсы Самое первое место, где программист сталкивается с необходимостью отде лить отображение от самих данных – это ресурсы. У каждого элемента есть свой ство Resources, представляющее собой обычный словарь2, который позволяет ис кать значение по ключу. Эта простая техника применяется в механизмах подде ржки тем, стилизации и привязки к данным. В программе на языке C# можно определять переменные для последующего использования. Часто это делается ради удобства чтения кода, а иногда в целях обобществления: public class Window1 : Window { public Window1() { Title = «Resources»; Brush toShare = new SolidColorBrush(Colors.Yellow); Button b = new Button(); b.Content = «My Button»; b.Background = toShare; Content = b; } } В разметке эта проблема не так проста. Модель грамматического разбора XAML требует, чтобы все создаваемые объекты были свойствами чеголибо. Захоти мы по 1 2 Единственный случай в WPF, где было решено не поддерживать преобразование данных, это привязка ресурсов. Обосновывается это тем, что ресурсы уже определены в области предс тавления, так что нужды в дополнительном преобразовании не возникает. Точнее, свойство Resources есть у любого класса, производного от FrameworkElement, FrameworkContentElement или Application. 298 Глава 6. Данные лучить кисть, общую для двух кнопок, пришлось бы создавать какойто вид привяз ки одного элемента к другому, а это, очевидно, куда сложнее, чем просто завести пе ременную. Поэтому при разработке WPF было решено ввести единое свойство Resources, в котором можно хранить список именованных объектов. Эти объекты стали бы тогда доступны любому дочернему элементу. В следующем фрагменте кисть определяется в свойстве Resources окна, а затем используется в кнопке: Рис. 6.1. Переопределение ресурсов в иерархии элементов <Window Text=’Resources’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’> <Window.Resources> <SolidColorBrush x:Key=’toShare’>Yellow</SolidColorBrush> </Window.Resources> <Button Background=’{StaticResource toShare}’ > My Button </Button> </Window> Принимая во внимание композицию элементов, мы хотели бы, чтобы поиск проводился с учетом иерархии. Если непосредственный родитель элемента не со держит некую переменную, то надо просматривать следующего родителя и т.д. В данном примере мы переопределяем один и тот же ресурс на нескольких уровнях дерева элементов. Сначала значение toShare определяется в корневом элементе Window, так же, как и раньше. Затем оно замещается во вложенной па нели StackPanel, где создается ресурс с таким же именем. Разные кнопки могут ссылаться на ресурс с именем toShare и получать разные значения в зависимости от того, в каком месте дерева находятся (рис. 6.1): Ресурсы 299 <Window x:Class=’EssentialWPF.Resources’ Title=’Resources’ Visibility=’Visible’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’> <Window.Resources> <SolidColorBrush x:Key=’toShare’>Yellow</SolidColorBrush> </Window.Resources> <StackPanel Margin=’10’> <Button Background=’{StaticResource toShare}’> Level 1 </Button> <StackPanel Margin=’10’> <StackPanel.Resources> <LinearGradientBrush x:Key=’toShare’ ...> <GradientStop Offset=’0’ Color=’sc# 1,1,1,1’ /> <GradientStop Offset=’1’ Color=’sc# 1,.6,.6,.6’ /> </LinearGradientBrush> </StackPanel.Resources> <Button Background=’{StaticResource toShare}’> Level 2 </Button> </StackPanel> <Button Background=’{StaticResource toShare}’> Level 1 </Button> </StackPanel> </Window> Путь поиска ресурса несколько сложнее, чем просто проход вверх по иеирар хии. Просматривается также объект приложения, системная тема и тема по умол чанию для типов. Порядок просмотра таков: 1. 2. 3. 4. Иерархия элементов. Application.Resources. Тема типа. Системная тема. Темы подробно рассматриваются в главе 8, а сейчас еще раз подчеркнем, что элемент, ссылающийся на ресурс, может получать данные из разных мест. Для автора приложения наиболее интересным аспектом этой функциональ ности, наверное, является возможность определять ресурсы на уровне прило жения. В главе 2 мы рассматривали объект Application. У него тоже есть свойство Resources, позволяющее определить ресурсы, глобальные для всего приложения: 300 Глава 6. Данные <Application x:Class=’EssentialWPF.MyApp’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ > <Application.Resources> <SolidColorBrush x:Key=’toShare’>Purple</SolidColorBrush> </Application.Resources> </Application> Эта техника позволяет создавать ресурсы, общие для всех страниц, окон и эле ментов управления На любом уровне приложения можно переопределить гло бальное значение ресурса; как это делается, мы уже видели. В общем случае реко мендуется определять ресурс на самом нижнем возможном уровне. Если некий ресурс используется только в одной панели, то для этой панели его и надо опре делить. Если же ресурс используется в нескольких окнах, то определить его сле дует на уровне приложения. Определяя ресурс, важно помнить, что использовать его можно в разных местах. Поскольку каждый элемент WPF может присутство вать только в одном месте дерева отображения, мы не можем надежно использо вать элемент в качестве ресурса: <Window x:Class=’EssentialWPF.Resources’ Title=’Resources’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’> <Window.Resources> <TextBox x:Key=’sharedTextBox’ /> </Window.Resources> <Button Content=’{StaticResource sharedTextBox}’/> </Window> Эта разметка будет работать только, если на элемент sharedTextBox есть толь ко одна ссылка. Попытайся мы воспользоваться этим ресурсом еще раз, приложе ние завершится с ошибкой: <Window x:Class=’EssentialWPF.Resources’ Title=’Resources’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’> <Window.Resources> <TextBox x:Key=’sharedTextBox’ /> </Window.Resources> <StackPanel> <!— это ошибка! —> <Button Content=’{StaticResource sharedTextBox}’/> <Button Content=’{StaticResource sharedTextBox}’/> </StackPanel> </Window> Ресурсы 301 В главе 3 мы узнали о том, что делать, когда ресурс требуется использовать бо лее одного раза, – прибегнуть к классу FrameworkElementFactory. Для элемен тов, принадлежащих шаблонам, мы создаем фабрику, а не сами элементы. Боль шинству визуальных объектов (кистей, перьев, сеток и т.д.) фабрика не нужна, так как многократное использование обеспечивается наследованием классу Freezable3. Возможно, вы задаетесь вопросом: «С чего вдруг он завел речь о ресурсах в главе, посвященной привязке к данным?» Дело в том, что, используя ссылки на статические ресурсы, мы по существу выполняем присваивание переменной, как в приведенном выше фрагменте на C#. Когда эта переменная используется, ника кой связи с исходной переменной уже нет. Рассмотрим следующий код: Brush someBrush = Brushes.Red; Button button1 = new Button(); button1.Background = someBrush; someBrush = Brushes.Yellow; // button1.Background здесь будет красным При работе с ресурсами можно либо выполнить аналогичное статическое свя зывание в форме присваивания, либо организовать динамическое связывание: <Window Title=’Resources’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’> <Window.Resources> <SolidColorBrush x:Key=’toShare’>Yellow</SolidColorBrush> </Window.Resources> <Button Background=’{DynamicResource toShare}’ > My Button </Button> </Window> Поскольку на этот раз мы воспользовались динамическим связыванием, то можем изменить цвет кнопки, присвоив новое значение свойству Resources окна: <!— window1.xaml —> ... <Button Background=’{DynamicResource toShare}’ Click=’Clicked’> My Button </Button> ... // window1.xaml.cs ... 3 Объекты класса Freezable поддерживают обобществление благодаря режиму «заморозки», в ко тором они не могут быть изменены. Режим заморозки дает возможность нескольким объектам пользоваться одним и тем же экземпляром, не думая о том, был ли он изменен кемто другим. 302 Глава 6. Данные void Clicked(object sender, RoutedEventArgs e) { Brush newBrush = new SolidColorBrush(Colors.Blue); this.Resources[«toShare»] = newBrush; } ... Это очень полезный механизм. В сочетании с иерархической областью види мости ресурсов он позволяет обновить сразу все окна или страницы приложения. Чтобы выполнить динамическое связывание ресурса программно, нам понадо бится метод FrameworkElement.SetResourceReference: button1.SetResourceReference( Button.BackgroundProperty, «toShare»); Конечно, такие динамические ссылки не обходятся без накладных расходов, поскольку система должна отслеживать изменения ресурса. При проектировании модели ресурсов мы стремились создать такой механизм, который мог бы широ ко использоваться в системе для обнаружения ресурсов, не вызывая серьезного падения производительности. Поэтому ресурсы оптимизированы для достаточно крупных изменений. При изменении любого ресурса обновляется все дерево. Поэтому статические и динамические ссылки на ресурсы можно использовать во многих местах, стои мость операции от количества ссылок не зависит. А вот часто изменять ресурсы для обновления пользовательского интерфейса не стоит. Зато и тревожиться по поводу большого числа ссылок на ресурсы не надо. Ресурсы – это особая форма привязки к данным, оптимизированная в расчете на большое число привязок, которые редко обновляются. В общем случае, меха низм привязки к данным оптимизирован в предположении умеренного числа привязок (в том числе и двусторонних) с высокой частотой обновления. Этот бо лее общий вид привязки получил и более простое название; в WPF он называет ся просто связыванием или привязкой. Основные принципы связывания Связывание – это просто способ синхронизации двух элементов данных. Элемент данных (data point) – абстрактное понятие, выражающее идею «точ ки» в пространстве данных. Описать элемент данных можно разными способа ми; чаще всего он представляется источником данных и запросом. Например, элемент данных «свойство» состоит из объекта и имени свойства. Имя свой ства определяет само свойство, а объект служит источником данных для этого свойства. В WPF элемент данных представлен классом Binding. Для конструирова ния привязки мы указываем источник (данных) и путь (запрос). В следующем примере создается элемент данных, ссылающийся на свойство Text объекта TextBox: Основные принципы связывания 303 Binding bind = new Binding(); bind.Source = textBox1; bind.Path = new PropertyPath(«Text»); Нужен еще второй элемент данных, который будет синхронизован с первым. Поскольку связывание в WPF ограничивается только деревом элементов, то для определения какоголибо элемента данных нужно вызвать метод SetBinding. Этот метод вызывается для источника данных, а данные привязываются к запросу4 (в данном примере к свойству ContentControl.ContentProperty): contentControl1.SetBinding(ContentControl.Content, bind); В этом примере свойство Text объекта textBox1 связывается со свойством Content объекта contentControl1. То же самое можно было бы выразить на XAML (рис. 6.2): <Window ... Title=’EssentialWPF’> <StackPanel> <TextBox x:Name=’textBox1’ /> <ContentControl Margin=’5’ x:Name=’contentControl1’ Content=’{Binding ElementName=textBox1,Path=Text}’ /> </StackPanel> </Window> Когда привязка объявляется в разметке, для задания источника можно ис пользовать свойство ElementName. Учитывая все, что мы узнали об элементах управления в главе 3, не должен вызывать удивления тот факт, что механизм связывания можно применить для привязки свойства Text (строкового) элемента TextBox к свойству Content (име ющему тип object). Удивительно же то, что мы можем привязать свойство Text к чемуто совершенно непохожему, например, к свойству FontFamily (строкой не являющемуся): <Window ... Title=’EssentialWPF’ > <StackPanel> <TextBox x:Name=’textBox1’ /> <TextBox x:Name=’textBox2’ /> <ContentControl Margin=’5’ Content=’{Binding ElementName=textBox1,Path=Text}’ FontFamily=’{Binding ElementName=textBox2,Path=Text}’ /> </StackPanel> </Window> 4 Внимательно следите за тем, чтобы передаваемое в качестве аргумента зависимое свойство было определено в том же типе, которому принадлежит объект, для которого вызывается ме тод SetBinding. Например, если в момент привязки к TextBox методу SetBinding передается свойство TextBlock.TextProperty вместо TextBox.TextProperty, то программа откомпилируется и будет работать, но связывания не произойдет. Глава 6. Данные 304 Рис. 6.2. Привязка TextBox к ContentControl Рис. 6.3. Привязка TextBox к FontFamily На рис. 6.3 показано, что значение преобразуется из строки в объект класса FontFamily. Существует два механизма преобразования: класс TypeConverter, су ществующий в .NET, начиная с версии 1.0, и новый интерфейс IValueConverter. В нашем случае с классом FontFamily ассоциирован конвертер типов TypeConverter, поэтому преобразование выполняется автоматически. Чтобы выполнить нужное преобразование, можно воспользоваться конверте рами значений, ассоциированными с привязкой (рис. 6.4). Для этого берется ис точник (строка из свойства Text) и преобразуется в какойто объект, который по нимает получатель (свойство Content). Основные принципы связывания 305 Конвертер значений Прямое преобразование Получатель Источник Обратное преобразование Рис. 6.4. Концептуальная модель связывания с помощью конвертера значений Начнем с создания простенького типа: public class Human { private string _name; public string Name { get { return _name; } set { _name = value; } } } Тип мог бы быть любым: встроенным, библиотечным, разработанным вами. Идея в том, что мы хотим преобразовать свойство Text в объект конкретного типа. Для это го произведем конвертер от интерфейса IValueConverter и реализуем два метода: public class HumanConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { Human h = new Human(); h.Name = (string)value; return h; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return ((Human)value).Name; } } В более сложных случаях реализовать преобразование в обе стороны может оказаться невозможно. Последний шаг при использовании конвертера – ассоци ировать его с привязкой: <ContentControl Margin=’5’ FontFamily=’{Binding ElementName=textBox2,Path=Text}’> <ContentControl.Content> <Binding ElementName=’textBox1’ Глава 6. Данные 306 Path=’Text’> <Binding.Converter> <l:HumanConverter xmlns:l=’clrnamespace:EssentialWPF’ /> </Binding.Converter> </Binding> </ContentControl.Content> </ContentControl> Рис. 6.5. Привязка с помощью конвертера значений Преобразование значений – лишь одна сторона вопроса. Но в нашем примере (см. рис. 6.5) хотелось бы сгенерировать полное отображение созданного типа. В главе 3 мы видели, что составные элементы допускают различные способы визу ализации данных. Самый универсальный – воспользоваться шаблоном для опре деления преобразования данных в дерево отображения. Шаблон данных принимает на входе данные (описываемые свойством DataType) и строит из них дерево отображения. Внутри дерева мы можем привя заться к частям элемента данных. В данном случае мы построим простой шаблон для типа Human и привяжем данные к свойству Name: <DataTemplate xmlns:l=’clrnamespace:EssentialWPF’ DataType=’{x:Type l:Human}’> <Border Margin=’5’ Padding=’5’ BorderBrush=’Black’ BorderThickness=’3’ CornerRadius=’5’> <TextBlock Text=’{Binding Path=Name}’ /> </Border> </DataTemplate> Этот шаблон можно ассоциировать с элементом ContentControl разными спо собами (например, с помощью ресурсов). Ниже мы просто присваиваем его свой ству ContentTemplate (рис. 6.6): <ContentControl Margin=’5’ Основные принципы связывания 307 FontFamily=’{Binding ElementName=textBox2,Path=Text}’> <ContentControl.Content> <Binding ... /> </ContentControl.Content> <ContentControl.ContentTemplate> <DataTemplate xmlns:l=’clrnamespace:EssentialWPF’ DataType=’{x:Type l:Human}’> ... </DataTemplate> </ContentControl.ContentTemplate> </ContentControl> Внимательно присмотревшись к этому шаблону, мы обнаружим, что для свя зывания не был определен источник данных: <TextBlock Text=’{Binding Path=Name}’ /> Это возможно, потому что WPF позволяет ассоциировать с элементами окру жающий контекст данных. В случае шаблонов данных в качестве контекста автома тически устанавливаются те данные, которые шаблон преобразует. Мы можем яв но задать свойство DataContext для любого элемента, и тогда указанный источник данных будет использован во всех привязках для этого элемента (и его потомков). Рис. 6.6. Связывание с помощью шаблона данных Элементы данных и преобразования – две базовые конструкции механизма связывания. Познакомившись с основными ингредиентами данных, мы можем заняться деталями привязки к объектам CLR и к XML, а затем углубиться в сис тему шаблонов данных. 308 Глава 6. Данные Привязка к объектам CLR Как мы видели, данные привязываются к объектам CLR с помощью свойств и списков (списком считается любой тип, реализующий интерфейс IEnumerable). Связывание устанавливает связь между источником и получателем. В случае при вязки к объекту, источник определяется путем к свойству, то есть последовательностью имен свойств и индексов, разделенных точками. Я тщательно следил за выбором слов; поначалу путь к свойству в смысле связывания можно перепутать с синтаксисом языка C#, поскольку в простейшей форме они действительно очень похожи. Синтаксис пу ти к связываемому свойству такой же, как уже встречался выше при обсуждении рас кадровок в главе 5, и мы еще вернемся к нему в главе 8, когда займемся стилями. Идентификатор имени свойства для связывания с объектом может записы ваться в двух видах: для простых свойств CLR и для зависимых свойств, произ водных от класса DependencyProperty, которые используются в WPF. Чтобы по нять, в чем разница, начнем с простого примера: <Window xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ > <StackPanel> <TextBox Name=’text1’>Hello</TextBox> <TextBox Text=’{Binding ElementName=text1, Path=Text}’ /> </StackPanel> </Window> Здесь свойство Text объекта TextBox связывается со свойством Text другого объекта. С очень похожим примером мы уже встречались выше. Поскольку Text – зависимое свойство, то этот пример в точности эквивалентен такому: <Window xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ > <StackPanel> <TextBox Name=’text1’>Hello</TextBox> <TextBox Text=’{Binding ElementName=text1, Path=(TextBox.Text)}’ /> </StackPanel> </Window> Во втором случае мы воспользовались формой идентификатора свойства, «классифицированной классом». Результат в обоих случаях одинаков, но во вто ром примере удается избежать применения отражения для разрешения имени «Text» в выражении привязки. Эта оптимизация полезна с двух точек зрения: во первых, чтобы избежать накладных расходов на отражение, а, вовторых, чтобы обеспечить привязку к присоединенным свойствам5. 5 Например, если бы мы захотели привязать объект TextBox к свойству Grid.Row, то могли бы написать <SomeControl SomeProperty=’{Binding ElementName=text1, Path=(Grid.Row)}’ />. Привязка к объектам CLR 309 Чтобы лучше понять, как работают пути к свойствам, мы можем взять чуть более сложный объект. Определим класс Person, в котором есть составные свойства Address и Name (рис. 6.7). В совокупности три класса – Person, Name и Address – образуют небольшую объектную модель, на которой можно про демонстрировать некоторые интересные задачи, возникающие в связи со свя зыванием. Для начала организуем простое отображение данных о человеке (рис. 6.8): <!— Window1.xaml —> <Window ... Title=’Object Binding’> <StackPanel> <ContentControl Content=’{Binding Path=Name}’ /> <TextBlock Text=’{Binding Path=Addresses[0].AddressName}’ /> <TextBlock Text=’{Binding Path=Addresses[0].Street1}’ /> <TextBlock Text=’{Binding Path=Addresses[0].City}’ /> <TextBlock Text=’{Binding Path=Addresses[0].State}’ /> <TextBlock Text=’{Binding Path=Addresses[0].Zip}’ /> </StackPanel> </Window> // Window1.xaml.cs public partial class Window1 : Window { public Window1(){ InitializeComponent(); DataContext = new Person( new Name( «Chris», «Anderson»), new Address( «Work», «One Microsoft Way», «Redmond», «WA», «98053»)); } } Здесь иллюстрируется привязка к простому свойству (Path=Name) и более сложные пути к свойствам (Path=Addresses[0].AddressName). Квадратные скоб ки позволяют добраться до отдельных элементов набора. Обратите также внима ние, что мы можем составить сколь угодно сложный путь из имен свойств и ин дексов в списке. Привязка к списку производится точно так же, как к свойству. Путь к свойству должен приводить к объекту, который реализует интерфейс IEnumerable, но в остальном никаких отличий нет. Можно было бы отобра жать не один адрес, а завести список и привязаться к его свойству ItemsSource (разумеется, тогда мы смогли бы определить шаблон данных для типа адреса): Глава 6. Данные 310 <ListBox ItemsSource=’{Binding Path=Addresses}’ /> Рис. 6.7. Объектная модель класса Person. У человека есть единственное имя и нуль или более адресов. Рис. 6.8. Составные пути привязки До сих пор нас интересовало в основном отображение данных. Если у свой ства, к которому мы привязываемся, есть метод установки, то возможна и двусто ронняя привязка. Привязка к объектам CLR 311 Редактирование Чтобы редактировать значения, должен быть какойто способ узнать, что зна чение изменилось. Помимо разрешения изменять свойство, существует несколь ко интерфейсов, которые позволяют объекту или списку рассылать извещения об изменении. Если источник данных уведомляет об изменении, то система связы вания сможет отреагировать на модификацию данных. Чтобы наделить наш класс Person способностью извещать об изменениях, у нас есть три возможности: (1) реализовать интерфейс INotifyPropertyChanged, (2) добавить события, с по мощью которых мы будем сообщать об изменении, (3) создать свойства, произ водные от класса DependencyProperty. Использование событий для извещения об изменениях свойств впервые было применено в .NET 1.0 и поддерживается механизмами связывания в Windows Forms и ASP.NET. Интерфейс INotifyPropertyChanged появился в .NET 2.0. Он оптимизирован для привязки к данным, обладает более высокой производитель ностью и проще как для авторов объектов, так и для самой системы связывания. Но использовать этот интерфейс в обычных сценариях, когда необходимо изве щать об изменениях, несколько сложнее. Применение свойств, производных от DependencyProperty, относительно просто, позволяет объекту воспользоваться преимуществами, которые дает раз реженное хранилище, и хорошо сопрягается с другими службами WPF (напри мер, с динамической привязкой к ресурсам и стилизацией). Подробно создание объектов со свойствами, производными от DependencyProperty, обсуждается в приложении. Все три способа во время выполнения демонстрируют одинаковое поведение. Вообще говоря, при создании модели данных лучше реализовать интерфейс INotifyPropertyChanged. Для использования свойств, производных от DependencyProperty, требуется, чтобы класс объекта наследовал DependencyObject, а это, в свою очередь, означает, что объект данных должен работать в STAпотоке. Применение событий для извещения об изменениях свойств приводит к разбуха нию объектной модели; в общем случае этот механизм лучше оставить для свойств, которые разработчики и так привыкли отслеживать: public class Name : INotifyPropertyChanged { ... public string First { get { return _first; } set { _first = value; NotifyChanged(«First»); } } ... public event PropertyChangedEventHandler PropertyChanged; Глава 6. Данные 312 void NotifyChanged(string property) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(property)); } } } Раз мы применяем один и тот же паттерн ко всем трем объектам данным, то можем изменить пользовательский интерфейс и воспользоваться преимуществами новой ре ализации объектов. Мы заведем два неизменяемых текстовых поля (TextBlock) для отображения текущего имени и два объекта TextBox для редактирования значений. Поскольку класс Name реализует интерфейс INotifyPropertyChanged, то при измене нии значений система связывания получит извещение, что приведет к обновлению объектов TextBlock (рис. 6.9): <Window ... Text=’Object Binding’> <StackPanel> <TextBlock Text=’{Binding Path=Name.First}’ /> <TextBlock Text=’{Binding Path=Name.Last}’ /> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Label Grid.Row=’0’ Grid.Column=’0’>First</Label> <TextBox Grid.Row=’0’ Grid.Column=’1’ Text=’{Binding Path=Name.First}’ /> <Label Grid.Row=’1’ Grid.Column=’0’>Last</Label> <TextBox Grid.Row=’1’ Grid.Column=’1’ Text=’{Binding Path=Name.Last}’ /> </Grid> </StackPanel> </Window> Ввод в любое текстовое поле обновляет соответствующую область окна. От метим, что изменение происходит только при выходе из поля. По умолчанию эле мент TextBox обновляет данные только в момент потери фокуса. Чтобы изменить это поведение, нужно указать, что привязка должно обновляться при любом из менении значения свойства. Для этого служит свойство привязки UpdateSourceTrigger: <TextBox Grid.Row=’1’ Grid.Column=’1’ Text=’{Binding Path=Name.Last,UpdateSourceTrigger=PropertyChanged}’ /> Привязка к объектам CLR 313 Рис. 6.9. Редактирование объекта с помощью двустороннего связывания Списки сложнее простого изменения свойств. Для связывания необходимо знать, какие элементы были добавлены или удалены. С этой целью в WPF введен интерфейс INotifyCollectionChanged, в котором определено единственное собы тие CollectionChanged с аргументом такого типа: public class NotifyCollectionChangedEventArgs : EventArgs { public NotifyCollectionChangedAction Action { get; } public IList NewItems { get; } public int NewStartingIndex { get; } public IList OldItems { get; } public int OldStartingIndex { get; } } public enum NotifyCollectionChangedAction { Add, Remove, Replace, Move, Reset, } В нашем примере мы можем поддержать динамическое добавление и удале ние адресов человека. Проще всего это реализуется в помощью класса ObservableCollection<T>, который наследует Collection<T> и дополнительно ре ализует интерфейс INotifyCollectionChanged: public class Person : INotifyPropertyChanged { IList<Address> _addresses = new ObservableCollection<Address>(); ... } 314 Глава 6. Данные Теперь можно модифицировать наше приложение так, чтобы все адреса отоб ражались в списковом поле, и реализовать пользовательский интерфейс для вво да новых адресов и добавления их в список: <!— window1.xaml —> <Window ... Text=’Object Binding’> <StackPanel> <StackPanel.Resources> <!— шаблон для отображения списка адресов —> <DataTemplate x:Key=’addressTemplate’> <StackPanel Orientation=’Horizontal’> <TextBlock Text=’{Binding Path=Street1}’ /> <TextBlock Text=’,’ /> <TextBlock Text=’{Binding Path=City}’ /> <TextBlock Text=’,’ /> <TextBlock Text=’{Binding Path=State}’ /> <TextBlock Text=’,’ /> <TextBlock Text=’{Binding Path=Zip}’ /> </StackPanel> </DataTemplate> </StackPanel.Resources> <!— имя человека —> <TextBlock Text=’{Binding Path=Name.First}’ /> <TextBlock Text=’{Binding Path=Name.Last}’ /> <!— список адресов —> <TextBlock Margin=’5’ FontSize=’14pt’>Addresses:</TextBlock> <ListBox ItemsSource=’{Binding Path=Addresses}’ ItemTemplate=’{DynamicResource addressTemplate}’ /> <!— добавляем новый ГИП для отображения адреса —> <TextBlock Margin=’5’ FontSize=’14pt’>Add Address:</TextBlock> <Grid Margin=’5’> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width=’Auto’ /> <ColumnDefinition Width=’*’ /> </Grid.ColumnDefinitions> <Label Grid.Row=’0’ Grid.Column=’0’>Street</Label> <TextBox Grid.Row=’0’ Grid.Column=’1’ Name=’_street’ /> <Label Grid.Row=’1’ Grid.Column=’0’>City</Label> New Привязка к объектам CLR 315 <TextBox Grid.Row=’1’ Grid.Column=’1’ Name=’_city’ /> <Label Grid.Row=’2’ Grid.Column=’0’>State</Label> <TextBox Grid.Row=’2’ Grid.Column=’1’ Name=’_state’ /> <Label Grid.Row=’3’ Grid.Column=’0’>Zip</Label> <TextBox Grid.Row=’3’ Grid.Column=’1’ Name=’_zip’ /> <Button Click=’Add’ Grid.Row=’4’>Add</Button> </Grid> </StackPanel> </Window> // window1.xaml.cs void Add(object sender, RoutedEventArgs e) { Address a = new Address(); a.Street1 = _street.Text; a.City = _city.Text; a.State = _state.Text; a.Zip = _zip.Text; ((Person)DataContext).Addresses.Add(a); } Эта программа выводит окно, показанное на рис. 6.10. В текстовые поля можно вводить новую информацию, а при нажатии кнопки Add список адресов обновляется. Поскольку свойство Addresses реализует ин терфейс INotifyCollectionChanged, система связывания получает извещение о но вом адресе и корректно обновляет пользовательский интерфейс. Рис. 6.10. Редактирование списка с использованием двустороннего связывания Глава 6. Данные 316 Привязка к объектам CLR – процедура по большей части автоматическая. Ре ализовав интерфейсы INotifyPropertyChanged и INotifyCollectionChanged, мы наделяем источник данных «интеллектом». В WPF также встроена поддержка для привязки к XML6. Привязка к XML Поддержка XML в WPF основана на объектной модели документа (DOM), реа лизованной в пространстве имен System.Xml. Мы можем привязаться к части любо го XMLдокумента, используя объекты XmlDocument, XmlElement или XmlNode в качестве источника. Свойства можно привязывать только к атрибуту или к содер жимому элемента; списки допускают привязку к любому набору элементов. Знакомство с XPath Если вы не знакомы с языком XPath, то имеет смысл потратить несколько ми нут на ознакомление с его синтаксисом, поскольку механизм связывания в WPF на него опирается. По существу, XPath – это язык запросов для выделения какой то части XMLдокумента. Выделить можно набор элементов, отдельный элемент или даже один атрибут элемента. Самым употребительным оператором в XPath является /. С его помощью строится путь к нужному элементу. Рассмотрим следующий XMLдокумент: <Media> <Book Author='John' Title='Fish are my friends' /> <Book Author='Dave' Title='Fish are my enemies' /> <Book Author='Jane' Title='Fish are my food' /> <CD Artist='Jane' Title='Fish sing well' /> <DVD Director='John' Title='Fish: The Movie'> <Actor>Jane</Actor> <Actor>Dave</Actor> </DVD> </Media> Оператор / позволяет сконструировать путь к любому элементу. Так, путь Media / CD ведет к элементу CD. По умолчанию любой текст в пути считается локальным име нем элемента. Для сложных XMLдокументов нужно указывать пространство имен XML, но для целей данного краткого введения мы ограничимся простыми документами. Выражение Media / CD в языке XPath означает «выбрать все элементы с именем ‘CD’, которые являются прямыми потомками любого элемента с именем ‘Media’». Будет по нятнее, если взглянуть, что возвращается, когда задан путь Media/CD: 6 Те, кто любит приключения, могут реализовать интерфейс ICustomTypeDescriptor, чтобы реа лизовать привязку к любой специализированной объектной модели. В сочетании с появив шимся в .NET 2.0 классом TypeDescriptorProvider уже существующие нестандартные объект ные модели будут без изменений работать с механизмом привязки в WPF. К сожалению, воп рос о реализации интерфейса ICustomTypeDescriptor выходит за рамки настоящей книги. Привязка к XML 317 <Book Author=’John’ Title=’Fish are my friends’ /> <Book Author=’Dave’ Title=’Fish are my enemies’ /> <Book Author=’Jane’ Title=’Fish are my food’ /> Выбраны все элементы Book. Тот факт, что XPath может порождать как спи сок узлов, так и отдельные узлы, оказывается очень важным для реализации при вязки к XML. В XML элементы и атрибуты считаются узлами. XPath может выбирать имен но узлы, а не только элементы. Чтобы обратиться к атрибуту, а не к элементу, не обходимо предпослать имени оператор @. Так, выражение Media/DVD/@Title возвращает узел «Fish: The Movie». Как и в случае выборки элементов, мы можем получить список атрибутов. Если задать путь Media/Book/@Title, то получим: Title=’Fish are my friends’ Title=’Fish are my enemies’ Title=’Fish are my food’ Тип данных каждого возвращенного в этом случае узла – XmlAttributeNode (это нам еще понадобится ниже). С помощью оператора * мы можем получить именованные узлы любого вида (атрибуты или элементы). Последний оператор XPath, который мы рассмотрим, – это []. Он позволяет выбирать узел по позиции или по атрибуту. Все, о чем мы говорили до сих пор, попрежнему применимо, но теперь мы можем еще и уточнить любое имя. Отме тим, что в XPath индексы нумеруются с единицы, а не с нуля, как в наборах CLR. Проще всего, указать позицию, например, Media/Book[1] возвращает: <Book Author=’John’ Title=’Fish are my friends’ /> Другое типичное применение Media/Book[@Author = «Jane»]: – выбор по атрибуту, например, <Book Author=’Jane’ Title=’Fish are my food’ /> Конечно, все эти способы можно комбинировать для создания сложных зап росов к неоднородным данным. Так, выражение */*[@Title = «Fish: The Movie»] отбирает любой элемент с заданным названием, в данном случае это будет DVD. В таблице 6.1 перечислены некоторые часто встречающиеся запросы XPath и примеры их применения к нашему документу. Привязка к XML Разобравшись с основами языка XPath, вернемся к вопросу о привязке к XML. Начнем с привязки к тому же документу, который мы взяли выше в каче стве примера. Но поначалу никакой реальной «привязки» не будет (рис. 6.11): public class Window1 : Window { public Window1() { XmlDocument doc = new XmlDocument(); Глава 6. Данные 318 doc.LoadXml(@» <Media xmlns=’’> <Book Author=’John’ Title=’Fish are my friends’ /> <Book Author=’Dave’ Title=’Fish are my enemies’ /> <Book Author=’Jane’ Title=’Fish are my food’ /> <CD Artist=’Jane’ Title=’Fish sing well’ /> <DVD Director=’John’ Title=’Fish: The Movie’> <Actor>Jane</Actor> <Actor>Dave</Actor> </DVD> </Media>»); ListBox list = new ListBox(); list.ItemsSource = doc.SelectNodes(«/Media/Book/@Title»); Title = «XML Binding»; Content = list; } } Таблица 6.1. Результаты применения различных XPathзапросов к XMLдокументу XPath Описание Пример Результаты / Выборка от корня / <Media xmlns=»»> <Book Author=»John» Title=»Fish are my friends» /> <Book Author=»Dave» Title=»Fish are my enemies» /> <Book Author=»Jane» Title=»Fish are my food» /> <CD Artist=»Jane» Title=»Fish sing well» /> <DVD Director=»John» Title=»Fish: The Movie»> <Actor>Jane</Actor> <Actor>Dave</Actor> </DVD> </Media> //NAME Сопоставляется с тегом NAME в любом потомке //Book <Book Author=»John» Title=»Fish are my friends» /> <Book Author=»Dave» Title=»Fish are my enemies» /> <Book Author=»Jane» Title=»Fish are my food» /> @NAME Сопоставляется с атрибутом NAME //Book/@Aut hor Author=»John» Author=»Dave» Author=»Jane» * Сопоставляется с любым тегом //&/@Author Author=»John» Author=»Dave» Author=»Jane» Привязка к XML 319 XPath Описание Пример Результаты @* Сопоставляется с любым атрибутом //Book/@* Author=»John» Title=»Fish are my friends» Author=»Dave» Title=»Fish are my enemies» Author=»Jane» Title=»Fish are my food» NAME Сопоставляется с тегом NAME /Media <Media xmlns=»»> <Book Author=»John» Title=»Fish are my friends» /> <Book Author=»Dave» Title=»Fish are my enemies» /> <Book Author=»Jane» Title=»Fish are my food» /> <CD Artist=»Jane» Title=»Fish sing well» /> <DVD Director=»John» Title=»Fish: The Movie»> <Actor>Jane</Actor> <Actor>Dave</Actor> </DVD> </Media> [] Отбирает потомков по позиции или по атрибуту //Book[1] <Book Author=»John» Title=»Fish are my friends» /> //Book[@Aut hor = «Jane»] <Book Author=»Jane» Title=»Fish are my food» /> Важно подчеркнуть, что связывание – неотъемлемая часть WPF. Модель списка и элементов, имеющих содержимое, изначально поддерживает отображе ние произвольных данных. Привязка к данным – это по сути своей способ пере нести показанный выше императивный код в декларативную форму, более удоб ную для инструментальных программ. Если переписать этот код с использовани ем связывания, то он будет выглядеть примерно так (на рис. 6.11 показан резуль тат выполнения): public class Window1 : Window { public Window1() { XmlDocument doc = new XmlDocument(); doc.LoadXml(...); XmlDataProvider dataSource = new XmlDataProvider(); dataSource.Document = doc; Binding bind = new Binding(); bind.Source = dataSource; bind.XPath = «/Media/Book/@Title»; Глава 6. Данные 320 ListBox list = new ListBox(); list.SetBinding(ListBox.ItemsSourceProperty, bind); Title = «XML Binding»; Content = list; } } Рис. 6.11. Текста нет!!! Важное различие между двумя этими примерами состоит в том, что, начав исполь зовать связывание, мы можем отслеживать изменения. Если модифицировать объект XmlDataProvider, то ListBox обновится автоматически. Кроме того, отказавшись от ре ализации выборки в императивном коде, мы перенесли ее в регулярный объект с изве стной семантикой, улучшили поддержку, которую могут оказать инструментальные средства, и сократили количество понятий, которыми должен владеть разработчик. Эквивалентная разметка записывается без особого труда: <Window ... Title=’XML Binding’> <Window.Resources> <XmlDataProvider x:Key=’dataSource’> <x:XData> <Media xmlns=’’> <Book Author=’John’ Title=’Fish are my friends’ /> <Book Author=’Dave’ Title=’Fish are my enemies’ /> <Book Author=’Jane’ Title=’Fish are my food’ /> <CD Artist=’Jane’ Title=’Fish sing well’ /> <DVD Director=’John’ Title=’Fish: The Movie’> <Actor>Jane</Actor> <Actor>Dave</Actor> </DVD> </Media> </x:XData> </XmlDataProvider> </Window.Resources> <ListBox ItemsSource = Шаблоны данных ‘{Binding dataSource}}’ /> </Window> 321 XPath=/Media/Book/@Title,Source={StaticResource Класс XmlDataProvider служит двум целям. Вопервых, он удобен для того, чтобы с помощью разметки создать объект XmlDocument и применить к нему вы ражение на языке XPath (мы можем фильтровать непосредственно источник дан ных). Вовторых, это обычный способ перенести XMLданные в источник дан ных (позже мы встретимся с источниками данных других типов). Часто можно просто использовать объект XmlDocument или XmlElement в качестве источника для привязки, вообще не создавая объект XmlDataProvider. Отметим следующую важную деталь: свойство Source привязки устанавливает ся через StaticResource. Сам объект Binding не является элементом, поэтому мы не можем устанавливать его свойства с помощью динамического ресурса. Если источ ник данных нужно определять динамически (либо с помощью ссылки на динами ческий ресурс, либо путем привязки), то имеется альтернатива – воспользоваться свойством DataContext (при этом заодно упрощается выражение привязки): <Window x:Class=’BookScratch.Window1’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Text=’XML Binding’ DataContext=’{DynamicResource dataSource}’ > <Window.Resources> <XmlDataProvider x:Key=’dataSource’> ... </XmlDataProvider> </Window.Resources> <ListBox ItemsSource= ‘{Binding XPath=/Media/Book/@Title}’ /> </Window> Познакомившись с привязкой к объектам CLR и к XML, мы можем перейти к следующей важной теме – шаблонам данных, которые дают возможность визуа лизировать данные. Мы использовали шаблоны данных во всех рассмотренных выше примерах, хотя иногда это было и незаметно. Шаблоны данных Мы уже видели, что шаблоны данных позволяют описать внешний вид дан ных. В этом разделе мы будем работать с привязкой к XML, но все изложенное сохраняет силу для привязки к данным любого типа. Чтобы понять суть шаблонов данных, вернемся к примеру, на котором мы изу чали привязку. Вместо того чтобы напрямую привязываться к атрибуту Title, привяжемся к элементам: 322 Глава 6. Данные public class Window1 : Window { public Window1() { XmlDocument doc = new XmlDocument(); doc.LoadXml(...); XmlDataProvider dataSource = new XmlDataProvider(); dataSource.Document = doc; Binding bind = new Binding(); bind.Source = dataSource; bind.XPath = «/Media/Book»; ListBox list = new ListBox(); list.SetBinding(ListBox.ItemsSourceProperty, bind); Title = «XML Binding»; Content = list; } } Так мы получаем на первый взгляд пустое списковое поле, которое на самом деле содержит три пустых элемента, в чем легко убедиться, щелкнув по списку мышью. Поскольку с объектом XmlElement не ассоциирован шаблон, то система просто вызывает его метод ToString, который возвращает пустую строку, потому что у элементов ниже «Book» нет потомков. Тип DataTemplate чемто напоминает ControlTemplate. Оба пользуются клас сом FrameworkElementFactory для определения дерева отображения. Но ControlTemplate определяет это дерево для элемента управления, внутри которо го мы пользуемся привязкой к шаблону для связывания отображаемых свойств со свойствами элемента. Что касается DataTemplate, то он определяет дерево отображения для элемента данных, внутри которого мы с помощью привязки к данным связываем отображаемые свойства со свойствами данных. Кроме того, DataTemplate автоматически устанавливает контекст данных для дерева отобра жения, а именно шаблонный элемент данных. Чтобы построить свой первый шаблон программно, подготовим простое текс товое поле, в котором будет отображаться название книги. Созданный объект DataTemplate будет ассоциирован с XmlElement как тип данных: DataTemplate template = new DataTemplate(); template.DataType = typeof(XmlElement); Далее нужно создать дерево отображения для шаблона: FrameworkElementFactory textFactory = new FrameworkElementFactory(typeof(TextBlock)); Со свойством TextTextProperty элемента TextBlock необходимо связать под ходящее выражение XPath: Binding bind = new Binding(); bind.XPath=»@Title»; textFactory.SetBinding(TextBlock.TextProperty, bind); Шаблоны данных 323 Отметим, что объявлять источник данных необязательно. Свойству DataContext созданного дерева отображения автоматически присвоена ссылка на элемент, к ко торому применяется шаблон. Теперь можно ассоциировать с шаблоном фабрику: template.VisualTree = textFactory; И последний шаг – записать в свойство ItemTemplate элемента ListBox ссыл ку на только что определенный шаблон. Если собрать все вместе, то получится простое приложение для отображения названий книг: public Window1() { XmlDocument doc = new XmlDocument(); doc.LoadXml(...); DataTemplate template = new DataTemplate(); template.DataType = typeof(XmlElement); Binding bind = new Binding(); bind.XPath=»@Title»; FrameworkElementFactory textFactory = new FrameworkElementFactory(typeof(TextBlock)); textFactory.SetBinding(TextBlock.TextProperty, bind); template.VisualTree = textFactory; XmlDataProvider dataSource = new XmlDataProvider(); dataSource.Document = doc; bind = new Binding(); bind.Source = doc; bind.XPath = «/Media/Book»; ListBox list = new ListBox(); list.ItemTemplate = template; list.SetBinding(ListBox.ItemsSourceProperty, bind); Title = «XML Binding»; Content = list; } У классов DataTemplate и ControlTemplate очень схожие функции и способы применения. Ссылки на ресурсы часто используются для привязки свойства ItemTemplate списка к шаблону. Написанный выше код выглядит гораздо проще в виде разметки: <Window ... xmlns:sx=’clrnamespace:System.Xml;assembly=System.Xml’ Title=’XML Binding’ DataContext=’{DynamicResource dataSource}’ > <Window.Resources> <XmlDataProvider x:Key=’dataSource’> ... </XmlDataProvider> Глава 6. Данные 324 <DataTemplate x:Key=’template’ DataType=’{x:Type sx:XmlElement}’> <TextBlock Text=’{Binding XPath=@Title}’ /> </DataTemplate> </Window.Resources> <ListBox ItemsSource= ‘{Binding XPath=/Media/Book }’ ItemTemplate=’{DynamicResource template}’ /> </Window> Шаблоны – это чрезвычайно мощный механизм визуализации данных. Поми мо построения сложных шаблонов, мы можем создать несколько шаблонов и ди намически переключаться с одного на другой. Выбор шаблона Обычно шаблон ассоциируется с некоторым элементом данных, но часто бы вает необходимо динамически решить, какой шаблон использовать, – исходя из значения какогото свойства (хотя в главе 7 мы узнаем о триггерах, которые по могают в этом отношении) или глобального состояния. Если нужно заменить шаблон целиком, то можно воспользоваться классом DataTemplateSelector. В этом классе имеется единственный метод SelectTemplate, который позво ляет реализовать любую логику выбора нужного шаблона. Мы можем найти шаблон в дочернем элементе (например, ListBox), вернуть какието «зашитые» в код шаблоны или даже динамически создать шаблон для каждого элемента списка. Начнем с создания класса, производного от DataTemplateSelector, в котором выполняется выбор одного из нескольких шаблонов. В данном случае мы просто анализируем свойство LocalName объекта XmlElement и ищем ресурс с этим име нем в контейнере: public class LocalNameTemplateSelector : DataTemplateSelector { public override DataTemplate SelectTemplate(object item, DependencyObject container) { XmlElement data = item as XmlElement; if (data != null) { return (FrameworkElement)container).FindResource(data.LocalName) as DataTemplate; } return null; } } На этапе инициализации мы построим три шаблона: коричневые прямоуголь ники для книг, серебристые круги для CD и синие круги для DVD. Поскольку се лектор шаблонов анализирует локальное имя объекта XmlElement, для каждого шаблона следует задать атрибут x:Key: Шаблоны данных 325 <DataTemplate x:Key=’Book’ DataType=’{x:Type sx:XmlElement}’> <StackPanel Orientation=’Horizontal’> <Rectangle Margin=’2’ Width=’14’ Height=’14’ Fill=’Brown’ /> <TextBlock VerticalAlignment=’Center’ Text=’{Binding XPath=@Title}’ /> </StackPanel> </DataTemplate> <DataTemplate x:Key=’CD’ DataType=’{x:Type sx:XmlElement}’> <StackPanel Orientation=’Horizontal’> <Ellipse Margin=’2’ Width=’14’ Height=’14’ Fill=’Silver’ /> <TextBlock VerticalAlignment=’Center’ Text=’{Binding XPath=@Title}’ /> </StackPanel> </DataTemplate> <DataTemplate x:Key=’DVD’ DataType=’{x:Type sx:XmlElement}’> <StackPanel Orientation=’Horizontal’> <Ellipse Margin=’2’ Width=’14’ Height=’14’ Fill=’Blue’ /> <TextBlock VerticalAlignment=’Center’ Text=’{Binding XPath=@Title}’ /> </StackPanel> </DataTemplate> Осталось ассоциировать селектор шаблона со списковым полем вместо стати ческого шаблона (при этом мы выберем все носители информации, а не только книги): <Window ... xmlns:sx=’clrnamespace:System.Xml;assembly=System.Xml’ Title=’XML Binding’ DataContext=’{DynamicResource dataSource}’ > <Window.Resources> <XmlDataProvider x:Key=’dataSource’> ... </XmlDataProvider> <DataTemplate x:Key=’Book’ DataType=’{x:Type sx:XmlElement}’> ... </DataTemplate> <DataTemplate x:Key=’CD’ DataType=’{x:Type sx:XmlElement}’> ... </DataTemplate> <DataTemplate x:Key=’DVD’ DataType=’{x:Type sx:XmlElement}’> ... </DataTemplate> </Window.Resources> <ListBox ItemsSource= ‘{Binding XPath=/Media/*}’> <ListBox.ItemTemplateSelector> <l:LocalNameTemplateSelector Глава 6. Данные 326 xmlns:l=’clrnamespace:EssentialWPF’ /> </ListBox.ItemTemplateSelector> </ListBox> </Window> На рис. 6.12 приведен результат выполнения этой разметки. Более сложное связывание Иерархическое связывание Все примеры, которые мы рассматривали до сих пор, были плоскими: список клиентов или информационных носителей. Но часто данные бывают организованы иерархически, как, например, XMLдокумент общего вида или файловая система. Поскольку WPF поддерживает композицию элементов, то первое, что приходит в голову, – воспользоваться этой композицией для построения дерева элементов. Рис. 6.12. Связывание с помощью селектора шаблонов Чтобы проверить эту идею, попробуем опросить файловую систему. Нам предстоит определить простой шаблон и поместить данные в списковое поле. Поскольку значением свойства ItemsSource должен быть набор, то мы возьмем начальный каталог и обернем его в массив: <!— filebrowser.xaml —> <Window ... Title=’EssentialWPF’> <ListBox ItemsSource=’{Binding}’> <ListBox.ItemTemplate> <DataTemplate> <TextBlock Text=’{Binding Path=Name}’ /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> Более сложное связывание 327 </Window> // filebrowser.xaml.cs public partial class FileBrowser : Window { public FileBrowser() { InitializeComponent(); DataContext = new DirectoryInfo[] { new DirectoryInfo(«c:\\windows») }; } } Рис. 6.13. Один каталог, отображаемый в списковом поле Эта программа выводит не слишком интересную картину, показанную на рис. 6.13. На самом деле нам хотелось бы пойти вглубь иерархии каталогов. В классе System.IO.DirectoryInfo нет свойства для получения дочерних элементов катало га, поэтому для извлечения требуемых данных мы создадим конвертер значений7: public class GetFileSystemInfosConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { try { if (value is DirectoryInfo) { return ((DirectoryInfo)value).GetFileSystemInfos(); } } catch {} return null; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } 7 Если вам интересно, скажу, что реализация заключена в try/catchблок, чтобы обработать (не слишком удачно) случай, когда доступ к каталогу запрещен изза отсутствия прав. 328 Глава 6. Данные Теперь можно перейти от простой привязки к свойству Name объекта DirectoryInfo к получению элементов, хранящихся внутри каталога. Поместим в наш шаблон данных списковое поле, внутри которого будет находиться еще один шаблон данных (да, вот так, все страньше и страньше!): <Window ... xmlns:l=’clrnamespace:EssentialWPF’ > ... <ListBox ItemsSource=’{Binding}’> <ListBox.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text=’{Binding Path=Name}’ /> <ListBox Height=’75’> <!— ItemsSource для внутреннего ListBox будет набором дочерних элементов. —> <ListBox.ItemsSource> <Binding Path=’.’> <Binding.Converter> <l:GetFileSystemInfosConverter /> </Binding.Converter> </Binding> </ListBox.ItemsSource> <!— Элементы во внутреннем ListBox просто TextBlocks. —> <ListBox.ItemTemplate> <DataTemplate> <TextBlock Text=’{Binding Path=Name}’ /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Window> Теперь получилась иерархия, показанная на рис. 6.14. Но это еще не конец. Чтобы получить структуру документа целиком, придется добавлять все новые и новые копии шаблона для каждого уровня иерархии. Если скопировать еще пару раз, получится картина, представленная на рис. 6.15. Хотя этот подход и работает, но у него есть проблемы. Вопервых, ни один эле мент управления ничего не знает об иерархии. Вспомните элемент TreeView или Menu, в которые иерархия встроена; для правильной работы они должны видеть эту иерархию. Вовторых, шаблон необходимо скопировать для каждого поддер живаемого уровня иерархии. Начнем с решения проблемы копирования. Вместо того чтобы определять шаблон внутри ListBox, мы можем поместить определение в ресурсы и восполь Более сложное связывание 329 зоваться ссылкой на ресурс для организации рекурсии (это напоминает ситуа цию, когда функция вызывает сама себя): Рис. 6.14. Простая иерархическая привязка с использованием вложенных шаблонов данных Рис. 6.15. Чем больше уровней иерархии, тем больше вложенных шаблонов данных <Window ... xmlns:io=’clrnamespace:System.IO;assembly=mscorlib’> <Window.Resources> <DataTemplate DataType=’{x:Type io:DirectoryInfo}’> <StackPanel> <TextBlock Text=’{Binding Path=Name}’ /> <ListBox> <ListBox.ItemsSource> <Binding Path=’.’> <Binding.Converter> <l:GetFileSystemInfosConverter /> </Binding.Converter> </Binding> </ListBox.ItemsSource> </ListBox> </StackPanel> </DataTemplate> Глава 6. Данные 330 </Window.Resources> <ListBox ItemsSource=’{Binding}’ /> </Window> Стоп, стоп! А где тут рекурсия? Напомним, что шаблон данных можно обна ружить с помощью свойства DataType. Это означает, что когда список ListBox видит элемент типа DirectoryInfo, он будет автоматически искать только что оп ределенный нами шаблон. А внутри этого шаблон есть вложенный список, кото рый будет делать то же самое. Voila! Приложение формирует точно такую же кар тину, как показано на рис. 6.15, но мы ушли от ручного копирования шаблона. Для таких элементов управления, как TreeView или Menu, которые поддерживают иерархию по определению, не хочется иметь десятки вложенных друг в друга объек тов. Чтобы сообщить элементу об иерархической природе данных, можно воспользо ваться классом HierarchicalDataTemplate. На самом деле это подкласс DataTemplate с дополнительными свойствами, которые мы ожидаем от объекта ItemsControl: public class HierarchicalDataTemplate : DataTemplate { public BindingBase ItemsSource { get; set; } public DataTemplate ItemTemplate { get; set; } public DataTemplateSelector ItemTemplateSelector { get; set; } } В случае простого рекурсивного шаблона, как, например, для файловой систе мы, мы можем воспользоваться тем фактом, что шаблоны данных автоматически ассоциируются с типом, и создать поразительно простой шаблон: <Window.Resources> <HierarchicalDataTemplate DataType=’{x:Type io:DirectoryInfo}’> <HierarchicalDataTemplate.ItemsSource> <Binding Path=’.’> <Binding.Converter> <l:GetFileSystemInfosConverter /> </Binding.Converter> </Binding> </HierarchicalDataTemplate.ItemsSource> <TextBlock Text=’{Binding Path=Name}’ /> </HierarchicalDataTemplate> </Window.Resources> <TreeView ItemsSource=’{Binding}’/> </Window> На первый взгляд, ничего непонятно. Разберемся. Содержимое HierarchicalDataTemplate – это шаблон для элемента списка. Это именно то, что должно отображаться для каждого каталога. Свойство ItemsSource сообщает шаблону, как получать дочерние элементы. Поскольку они тоже являются объек тами типа DirectoryInfo, система связывания автоматически применит к ним тот же шаблон. Результат показан на рис. 6.16. Более сложное связывание 331 Рис. 6.16. Применение HierarchicalDataTemplate в качестве источника данных для элемента управления TreeView Класс HierarchicalDataTemplate можно использовать даже тогда, когда дан ные неоднородны. Просто для каждого типа в иерархии определяется отдельный шаблон (а не один и тот же для всех). Представления наборов До сих пор речь шла о трех объектах, принимающих участие в связывании: ис точник данных, привязка и целевой элемент. Но в случае привязки к списку есть и четвертый игрок: представление набора (collection view). Оно отвечает за отс леживание текущего элемента, а также за фильтрацию, сортировку и группиров ку данных. Можно считать, что представление набора – это тонкая обертка вок руг данных, которая позволяет взглянуть на одни и те же данные поразному. Для небольших списков, которые только и встречаются в этой книге, это не столь важ но, но если набор данных велик, то очень важно не загружать его в память более одного раза. Управление текущим элементом Самый важный аспект представления набора – это отслеживание текущего элемента в списке. При любой привязке к списку система по умолчанию созда ет представление набора. Для того чтобы понять, как работает этот механизм, проще всего воспользоваться свойством IsSynchronizedWithCurrentItem спис кового элемента управления. Оно синхронизирует выбранный в списке эле мент с текущим элементом представления. Обычно мы привязываем набор к свойству, тип значения которого –набор (например, к свойству ItemsSource). Можно привязать весь набор, индексированный элемент набора или свойство текущего элемента. Мы уже видели привязку всего набора и индексированного элемента. А для того чтобы привязать свойство текущего элемента, достаточно указать его имя. Отсутствие квадратных скобок означает, что привязывается текущий элемент. Глава 6. Данные 332 public class AdvancedBinding : Window { public AdvancedBinding() { StackPanel sp = new StackPanel(); Person[] names = new Person[] { new Person(new Name(«Chris», «Anderson»)), new Person(new Name(«Don», «Box»)), new Person(new Name(«Chris», «Sells»)), }; ListBox list = new ListBox(); list.IsSynchronizedWithCurrentItem = true; list.DisplayMemberPath = «Name»; list.ItemsSource = names; TextBlock selected = new TextBlock(); selected.FontSize = 24; Binding text = new Binding(); text.Source = names; text.Path = new PropertyPath(«Name»); selected.SetBinding(TextBlock.TextProperty, text); sp.Children.Add(list); sp.Children.Add(selected); Content = sp; } } На рис. 6.17 показано появляющееся окно; когда мы выбираем в списковом поле некий элемент, текстовый блок обновляется. Рис. 6.17. Использование ListBox для демонстрации управления текущим элементом В данном случае мы воспользовались представлением по умолчанию для на бора имен. Обратиться к этому представлению из программы позволяет стати ческий метод GetDefaultView класса CollectionViewSource: Более сложное связывание 333 public class AdvancedBinding : Window { public CollectionViews() { StackPanel sp = new StackPanel(); ... WrapPanel commands = new WrapPanel(); Button prev = new Button(); prev.Content = «<»; prev.Click += delegate { ICollectionView view = CollectionViewSource.GetDefaultView(names); view.MoveCurrentToPrevious(); }; commands.Children.Add(prev); Button next = new Button(); next.Content = «>»; next.Click += delegate { ICollectionView view = CollectionViewSource.GetDefaultView(names); view.MoveCurrentToNext(); }; commands.Children.Add(next); sp.Children.Add(commands); ... } } Управление текущим элементом – самая важная функция представления на бора. Остальные связаны с организацией виртуального представления данных. Оно дает некоторым элементам управления (например, сетке) возможность пре доставить ряд ожидаемых средств (к примеру, сортировку путем щелчка по заго ловку столбца). Фильтрация, сортировка и группировка Фильтрация – самая простая из всех функций представления; мы определяем об ратный вызов в виде делегата, который решает, надо ли показывать элемент набора. Как всегда, можно воспользоваться представлением по умолчанию, модифицировать его или создать новое. Чтобы стало понятно, как обрабатываются элементы, возьмем два списка: один с подразумеваемым представлением (немодифицированным), а другой – со специальным представлением, в котором реализованы некие хитрости. Сначала создадим собственное представление, которое отфильтровывает всех носящих имя Don: ICollectionView view = new ListCollectionView(names); view.Filter = Глава 6. Данные 334 delegate(object item) { return ((Person)item).Name.First != «Don»; }; Рис. 6.18. Фильтрация элементов списка Чтобы показать это представление, запишем его в свойство ItemsSource спис ка. На рис. 6.18 показано, что в результате из списка удаляется один элемент: public class CollectionViews : Window { public CollectionViews() { StackPanel sp = new StackPanel(); Person[] names = new Person[] { new Person(new Name(«Chris», «Anderson»)), new Person(new Name(«Don», «Box»)), new Person(new Name(«Chris», «Sells»)), new Person(new Name(«Brent», «Anderson»)), new Person(new Name(«Dave», «Sells»)), }; sp.Children.Add(new TextBlock(new Run(«Unmodified»))); ListBox list = new ListBox(); list.DisplayMemberPath = «Name»; list.ItemsSource = names; sp.Children.Add(list); sp.Children.Add(new TextBlock(new Run(«Modified»))); ListBox modified = new ListBox(); ICollectionView view = new ListCollectionView(names); Более сложное связывание 335 view.Filter = delegate(object item) { return ((Person)item).Name.First != «Don»; }; modified.DisplayMemberPath = «Name»; modified.ItemsSource = view; sp.Children.Add(modified); Content = sp; } } Сортировка также не вызывает трудностей. Добавив один или несколько объектов SortDescription, мы сможем отсортировать список по любому числу свойств. В приме ре ниже сортировка производится сначала по фамилии, а потом по имени (рис. 6.19): view.SortDescriptions.Add( new SortDescription(«Name.Last», ListSortDirection.Ascending)); view.SortDescriptions.Add( new SortDescription(«Name.First», ListSortDirection.Ascending)); Последняя функция – группировка – нуждается в поддержке как со стороны представления набора, так и со стороны элемента управления, который привязы вается к данным. С точки зрения представления набора группировка работает почти так же, как сортировка: мы добавляем один или несколько объектов GroupDescription, которые определяют, как данные разбиваются на группы: view.GroupDescriptions.Add(new PropertyGroupDescription(«Name.Last»)); Рис. 6.19. Сортировка данных в представлении Глава 6. Данные 336 На стороне же элемента управления необходим какойто способ визуализиро вать группы. Все списковые элементы управления в WPF обладают свойством GroupStyle, которое предназначено как раз для этой цели (рис. 6.20): GroupStyle style = new GroupStyle(); style.HeaderTemplate = new DataTemplate(); style.HeaderTemplate.VisualTree = new FrameworkElementFactory(typeof(TextBlock)); style.HeaderTemplate.VisualTree.SetBinding( TextBlock.TextProperty, new Binding(«Name»)); style.HeaderTemplate.VisualTree.SetValue( TextBlock.BackgroundProperty, Brushes.Silver); modified.GroupStyle.Add(style); Рис. 6.20. Группировка данных в представлении В этом примере мы применили класс PropertyGroupDescription, который оп ределяет группировку на базе значения некоторого свойства. Не слишком слож но самостоятельно реализовать логику группировки, унаследовав классу GroupDescription и переопределив метод GroupNameFromItem. Преобразование всего этого в разметку производится в общемто прямоли нейно, но есть несколько интересных подводных камней. Чтобы описать предс тавление набора в разметке, необходимо использовать тип CollectionViewSource: <Window ... Title=’EssentialWPF’ > Более сложное связывание 337 <Window.Resources> <CollectionViewSource x:Key=’customView’ /> </Window.Resources> </Window> Чтобы привязать данные к представлению набора в качестве источника, мы должны установить свойство Source. Следуя духу исходного примера, опишем источник как массив объектов типа Person. Зададим также контекст данных для окна: <Window ... xmlns:l=’clrnamespace:EssentialWPF’ Title=’EssentialWPF’ DataContext=’{DynamicResource dataSource}’ > <Window.Resources> <x:Array Type=’{x:Type l:Person}’ x:Key=’dataSource’> <l:Person> <l:Person.Name> <l:Name First=’Chris’ Last=’Anderson’ /> </l:Person.Name> </l:Person> <!— прочие люди —> </x:Array> <CollectionViewSource x:Key=’customView’ Source=’{StaticResource dataSource}’ /> </Window.Resources> </Window> Для присваивания значения свойству Source необходимо использовать стати ческий ресурс StaticResource, так как это свойство не является зависимым. Но для задания свойства DataContext для окна мы в этом случае обязаны пользо ваться динамическим ресурсом DynamicResource, так как определение dataSource находится внутри окна (DynamicResource допускает такого рода опе режающие ссылки). Для фильтрации CollectionViewSource применяется собы тие, а не простой обратный вызов через делегата, поэтому придется немного из менить определение фильтра на C#: public void NotDon(object sender, FilterEventArgs e) { e.Accepted = ((Person)e.Item).Name.First != «Don»; } Теперь добавление фильтра сводится просто к присоединению обработчика события к источнику CollectionViewSource: <CollectionViewSource x:Key=’customView’ Filter=’NotDon’ Source=’{StaticResource dataSource}’ /> 338 Глава 6. Данные Сортировка осложняется только тем фактом, что класс SortDescription опре делен в пространстве имен System.ComponentModel, которое обычно не включа ется в стандартную для WPF директиву xmlns: <CollectionViewSource x:Key=’customView’ xmlns:cm=’clrnamespace:System.ComponentModel;assembly=WindowsBase’ Filter=’NotDon’ Source=’{StaticResource dataSource}’> <CollectionViewSource.SortDescriptions> <cm:SortDescription PropertyName=’Name.Last’ Direction=’Ascending’ /> <cm:SortDescription PropertyName=’Name.First’ Direction=’Ascending’ /> </CollectionViewSource.SortDescriptions> </CollectionViewSource> Группировка описывается тривиально, никаких расхождений с ожиданиями нет: <CollectionViewSource x:Key=’customView’ ... > ... <CollectionViewSource.GroupDescriptions> <PropertyGroupDescription PropertyName=’Name.Last’ /> </CollectionViewSource.GroupDescriptions> </CollectionViewSource> Списковое поле привязывается к представлению набора через стандартное свойство ItemsSource. Кстати, хотя представление можно использовать напря мую как значение свойства ItemsSource, для привязки к нему в качестве источни ка необходим объект Binding: <ListBox DisplayMemberPath=’Name’ ItemsSource=’{Binding Source={StaticResource customView}}’> <ListBox.GroupStyle> <GroupStyle HeaderTemplate=’{StaticResource groupHeader}’ /> </ListBox.GroupStyle> </ListBox> Представления наборов обеспечивают эффективное обобществление данных, а также различные виды данных. Использовать их во всех сценариях привязки необязательно, но при правильном применении они могут заметно повысить про изводительность приложения. Отображение, управляемое данными В большинстве приложений автор конструирует простой пользовательский ин терфейс, создает модель данных, а затем объединяет то и другое с помощью связы Отображение, управляемое данными 339 вания. Но WPF позволяет и обратить это привычное отношение: данные можно сделать первичными, а интерфейс вторичным. В результате удается построить раз витую визуализацию данных, независимую от способа использования. Ключом к этой модели служат три класса: ContentControl, ItemsControl и DataTemplate. Начнем с простой модели данных – изображения. В главе 5 мы видели, что для работы с изображениями есть два основных типа данных: элемент управления Image и иерархия классов, производных от ImageSource (в том числе и наиболее употребительный класс BitmapImage). В пользовательских интерфейсах элемент Image применяется чаще всего. И самый простой способ его применения – запи сать URI изображения в свойство Source: <Image Source=’c:\data\book\images\flowers.jpg’ /> Свойство Source на самом деле определено в классе ImageSource, и мы можем задать в качестве его значения прямо BitmapImage, если хотим управлять деталя ми загрузки (например, декодировать разрешающую способность): <Image> <Image.Source> <BitmapImage UriSource=’c:\data\book\images\flowers.jpg’ /> </Image.Source> </Image> А теперь обратим эту модель: вместо того чтобы использовать элемент Image и запол нять его данными, начнем с самих данных (изображения) и посмотрим, как их отобра зить. Поскольку изображение только одно, воспользуемся элементом ContentControl: <ContentControl> <ContentControl.Content> <BitmapImage UriSource=’c:\data\book\images\flowers.jpg’ /> </ContentControl.Content> </ContentControl> Эта программа выводит строку, как показано на рис. 6.21, – не слишком интересно. Рис. 6.21. Вывод растрового изображения с помощью ContentControl Здесь элемент ContentControl видит данные, в данном случае растровое изоб ражение, и вызывает метод ToString для формирования визуального отображе 340 Глава 6. Данные ния. После добавления шаблона данных картинка перестает быть такой скучной. Сначала определим простой шаблон, в котором для вывода изображения приме няется элемент Image (рис. 6.22): <DataTemplate DataType=’{x:Type BitmapImage}’> <Image Source=’{Binding}’ /> </DataTemplate> Рис. 6.22. Вывод растрового изображения по более интересному шаблону Как видно из рис. 6.22, отображение стало куда интереснее. Но по существу оно ничем не отличается от того, что мы имели с самого начала при использова нии элемента Image. Добавим немного разнообразия (рис. 6.23): <DataTemplate DataType=’{x:Type BitmapImage}’> <Border> <Border HorizontalAlignment=’Center’ VerticalAlignment=’Center’ Margin=’4’ BorderThickness=’1’ BorderBrush=’Black’ Padding=’4’ Background=’White’> <Border.BitmapEffect> <DropShadowBitmapEffect Softness=’.2’ ShadowDepth=’1’/> Отображение, управляемое данными 341 </Border.BitmapEffect> <Image Source=’{Binding}’ /> </Border> </Border> </DataTemplate> Рис. 6.23. Вывод растрового изображения с дополнительными элементами Можно было бы пойти еще дальше, добавив средства для сдвига картинки и другие полезные вещи (рис. 6.24). Важно отметить, что модель данных, описывающая изображение, не измени лась. Представьте на секунду, что вместо BitmapImage мы взяли бы в качестве данных объект Customer. Можно было бы создать модель данных клиента, опи сав свойства, поведение и т.д., а затем сконструировать различные пользовательс кие интерфейсы для взаимодействия с этими данными. Глава 6. Данные 342 Конструировать интерфейс, исходя из данных, хорошо тем, что получающееся представление можно повторно использовать в любом месте приложения. Техника программирования, которую можно условно назвать «сначала данные», применима на многих уровнях. Пусть, например, нам нужно вывести список изображений. Пона чалу возникает искушение воспользоваться списковым полем и зашить список изоб ражений в код. В том месте, где список понадобился в первый раз, это неплохо и срав нительно просто, но, если его нужно отображать в нескольких местах, то хотелось бы повторно использовать определение способа отображения списка. Возможно, это бу дет обычное списковое поле с горизонтальной или вертикальной прокруткой, а, быть может, элемент ListView с несколькими колонками, в которых выводятся дополни тельные данные. Если мы начнем с определения нового типа для списка изображений, то сможем создать для него шаблон точно так же, как для одного изображения: using System.Collections.ObjectModel; public class ImageList : ObservableCollection<BitmapImage> {} Рис. 6.24. Более сложные дополнительные элементы, в том числе действия и еще одно связывание Отображение, управляемое данными 343 Выше в этой главе мы уже видели, что класс ObservableCollection – это реали зация списка, которая поддерживает извещения об изменениях и все необходи мые средства для удобной работы в контексте разметки. Таким образом, мы мо жем определить, что содержимым окна будет просто список изображений: <Window ...> <l:ImageList> <BitmapImage ... /> <BitmapImage ... /> <BitmapImage ... /> </l:ImageList> </Window> Рис. 6.25. Повторное использование сложного шаблона в списке Чтобы описать, как этот список должен выглядеть (рис. 6.25), мы проделаем то же самое, что для одного изображения: <DataTemplate DataType=’{x:Type l:ImageList}’> <ListBox ItemsSource=’{Binding}’ /> </DataTemplate> 344 Глава 6. Данные Перейдя на управляемую данными модель определения внешнего вида прило жения, мы четко отделили модель данных от пользовательского интерфейса. В результате изменять облик приложения стало гораздо проще. Наличие явного контракта между моделью данных и моделью пользовательского интерфейса так же помогает лучше понять, когда внесенное изменение может привести к сбоям в интерфейсе. Чего мы достигли? В этой главе мы рассмотрели, как в WPF устроена работа с данными прило жения. Система привязки к данным глубоко интегрирована в платформу, и при наличии подходящей модели мы можем создавать приложения, целиком управ ляемые данными. 345 Глава 7. Действия В главе 6 мы видели, как можно включать в приложение данные и визуализи ровать их различными любопытными способами. До сих пор нас интересовало главным образом, как в WPF устроен вывод; мы занимались конструированием приложения из элементов управления, которые пользуются визуальными эле ментами для собственного отображения и менеджерами размещения для позици онирования. Теперь обратим свой взгляд на организацию ввода. Часто мы хотим, чтобы приложение определенным образом реагировало на движения мышью, нажатия кнопок или клавиш либо рисование пером. На платформе Windows Presentation Foundation есть три способа работы с действиями: события, команды и триггеры. Мы познакомимся с принципами, применимыми ко всем этим механизмам, в затем углубимся в детали каждого в отдельности. Принципиальные основы действий В том, что касается действий, WPF опирается на три принципа, которые на са мом деле служат естественным продолжением тех, что мы уже рассматривали: композиция элементов, слабая связь и декларативное описание. Для того чтобы действия были совместимы со способом построения дерева отображения, в кото ром одни элементы включают в себя другие, композиция является непременным условием. Тот факт, что элемент управления может радикально изменить свой внешний облик, порождает сложности в случае, когда источник события и код его обработки тесно связаны между собой; поэтому механизм ослабления этой связи также необходим. И, наконец, поскольку декларативная модель програм мирования пронизывает всю систему, WPF обязана поддерживать и декларатив ную обработку действий тоже. Композиция элементов В главе 3 мы убедились, что кнопка на самом деле составлена из нескольких элементов. Изза наличия подобной композиции возникает интересная проблема при обработке событий. Напомним три принципа построения элементов управ ления: композиция элементов, повсеместность развитого содержимого и простая модель программирования. Прежде всего, памятуя о простоте модели программирования, мы хотим, что бы код подписки на событие нажатия кнопки был прост и хорошо знаком разра ботчикам. Мы не должны требовать ничего сверх обычного присоединения обра ботчика к событию Click: Глава 7. Действия 346 Button b = new Button(); b.Content = «Click Me»; b.Click += delegate { MessageBox.Show(«You clicked me!»); }; На первый взгляд, выглядит замечательно, но посмотрим, что тут реально происходит. Ведь щелкаем мы не по кнопке, а по какомуто из элементов, из ко торых кнопка составлена. Чтобы все работало гладко, в WPF введено понятие маршрутизации события, которое позволяет обойти составляющие элементы. Продемонстрировать его мы сможем, добавив в предыдущем примере вторую кнопку, которая будет выступать в качестве содержимого. Щелчок как по первой, так и по второй кнопке приводит к возбуждению события: Button b = new Button(); b.Content = new Button(); b.Click += delegate { MessageBox.Show(«You clicked me!»); }; Получающееся дерево отображение показано на рис. 7.1, из которого также видно, что нам следует озаботиться не только двумя кнопками. Даже в простом случае единственной кнопки композиция событий необходима для того, чтобы маршрутизировать событие мыши до кнопки. Композиция элементов отражается на всех аспектах обработки действий, а не только на событиях. Слабая связь Из описания событий в интерфейсе класса Button следует, что он поддержи вает не только непосредственные события мыши (MouseUp, MouseDown и т.д.), но и событие Click, которое является абстракцией гораздо более высокого уров ня, чем просто событие мыши. Оно возникает и тогда, когда пользователь нажи мает пробельную клавишу (при условии, что кнопка владеет фокусом) или кла вишу Enter (если это кнопка по умолчанию для окна). Нажатие кнопки – это се мантическое событие, тогда как события мыши относятся к физическим. Рис. 7.1. Дерево отображения (слева) кнопки, в которую вложена другая кнопка (справа) Принципиальные основы действий 347 У написания кода для обработки именно события Click есть два преимущест ва: (1) мы не привязываемся к конкретному жесту пользователя (операции с мышью или клавиатурой) и (2) мы не ограничиваем себя только кнопкой. Каж дый из элементов CheckBox, RadioButton, Button и Hyperlink поддерживает на жатие. Если обработчик связан с событием Click, то он применим к любому ком поненту, который может быть «нажат». Такое отделение кода от действия обеспе чивает большую гибкость в реализации обработчиков. Однако самим событиям присуща некая форма зависимости; требуется, чтобы методобработчик имел вполне определенную сигнатуру. Например, делегат для обработки события Button.Click определен следующим образом: public delegate void RoutedEventHandler(object sender, RoutedEventArgs e); Одна из целей WPF – поддержать широкий спектр действий (рис. 7.2): от тес но связанных физических событий (например, MouseUp) до чисто семантичес ких извещений (например, команды ApplicationCommands.Close, которая сигна лизирует о том, что окно должно быть закрыто). событие MouseUp тесно событие Click команда Close слабо Рис. 7.2. Примеры, иллюстрирующие действия на разных концах спектра Допуская слабую связь, мы получаем возможность писать шаблоны, которые радикально меняют элемент управления. Например, включив кнопку, ассоци ированную с командой Close, мы сможем написать шаблон для окна, который до бавляет элемент для закрытия: <ControlTemplate TargetType=’{x:Type Window}’> <DockPanel> <StatusBar DockPanel.Dock=’Bottom’> <StatusBarItem> <Button Command=’{x:Static ApplicationCommands.Close}’> Close </Button> </StatusBarItem> </StatusBar> <ContentPresenter /> </DockPanel> </ControlTemplate> Теперь можно заставить окно закрываться, когда любой компонент пошлет коман ду Close; для этого достаточно добавить в код класса окна соответствующую привязку: Глава 7. Действия 348 public MyWindow() { InitializeComponent(); CommandBindings.Add( new CommandBinding( ApplicationCommands.Close, CloseExecuted ) ); } void CloseExecuted(object sender, ExecuteRoutedEventArgs e) { this.Close(); } Команды – это наименее связанная модель действий в WPF. Слабая связь обеспечивает полное абстрагирование от источника действия (в данном случае кнопки) и от обработчика действия (в данном случае окна). Мы могли бы изме нить стиль окна, воспользовавшись совершенно другим элементом управления, и при этом ничего не сломалось бы. Декларативные действия Мы видим, что с появлением команд и слабой связи WPF движется в направ лении модели, когда программа просто объявляет о своих пожеланиях (например, «я хочу, чтобы окно закрылось, когда вы отдадите эту команду») вместо реализа ции (например, «вызвать метод Window.Close() при нажатии этой кнопки»). Одним из столпов WPF является идея декларативного программирования. Помимо визуальных элементов и структуры пользовательского интерфейса, в разметке можно выразить немалую долю программной логики. Декларативная логика особенно полезна, потому что, отталкиваясь от декларативного формата, мы часто можем предоставить более развитые инструменты и, возможно, более содержательные системные службы. Для разных способов обработки действий предусмотрены различные уровни поддержки в декларативной программе. Для событий можно в разметке объявить отвечающую на него функцию, но сам обработчик должен быть реализован в коде. Команды специально задуманы для декларативного использования, поскольку предлагают наилучшее отделение источника действия от его потребителя. У триг геров, пожалуй, самая развитая декларативная поддержка, но им недостает расши ряемости, поэтому применять их для решения сложных задач затруднительно. Все механизмы работы с действиями в той или иной мере поддерживают пе речисленные выше принципы. Углубленное изучение действий мы начнем с наи более известного: событий. События В WPF события ведут себя точно так же, как в любой другой библиотеке клас сов, входящей в состав .NET. Каждый объект предоставляет набор событий, на ко События 349 торые можно подписаться, определив соответствующий делегат. Мы уже отмечали, что в WPF имеется дополнительный механизм маршрутизации событий, который позволяет им распространяться вверх по дереву элементов. Существует три вида маршрутизации событий: прямая, всплытие (bubbling) и туннелирование (tunnel ing). Прямые события – это простые события, возникающие от одиночного источ ника, они почти идентичны стандартным событиям .NET с тем отличием, что реги стрируются в системе маршрутизации событий WPF1. Некоторые средства плат формы (например, триггеры) требуют, чтобы событие было явно зарегистрировано. Всплывающие и туннельные события – две стороны одной медали: туннельные события продвигаются от корня дерева к целевому элементу, а всплывающие – в обратном направлении. Обычно эти два вида событий встречаются попарно, причем туннельная версия имеет префикс Preview. Большинство событий ввода (от клави атуры, от мыши и от пера) имеют как туннельную, так и всплывающую версии, нап ример: MouseRightButtonDown и PreviewMouseRightButtonDown, соответственно. Написав небольшой пример, в котором создается иерархия элементов и обра батываются некоторые события, мы сможем увидеть, как соотносятся между со бой всплывающие и туннельные события. Ниже создается окно с группирующей рамкой и двумя кнопками: <Window ... PreviewMouseRightButtonDown=’WindowPreviewRightButtonDown’ MouseRightButtonDown=’WindowRightButtonDown’ > <GroupBox PreviewMouseRightButtonDown=’GroupBoxPreviewRightButtonDown’ MouseRightButtonDown=’GroupBoxRightButtonDown’ > <StackPanel> <Button>One</Button> <Button PreviewMouseRightButtonDown=’ButtonTwoPreviewRightButtonDown’ MouseRightButtonDown=’ButtonTwoRightButtonDown’ > Two </Button> </StackPanel> </GroupBox> </Window> В обработчике каждого события мы можем вывести его имя: void ButtonTwoPreviewRightButtonDown(object sender, MouseButtonEventArgs e) { Debug.WriteLine(«ButtonTwo PreviewRightButtonDown»); } 1 Система маршрутизации отвечает в WPF за продвижение событий по дереву элементов. Боль% шая часть этой системы скрыта, видимы лишь небольшие островки, например: EventManager, RegisterRoutedEvent. Глава 7. Действия 350 void ButtonTwoRightButtonDown(object sender, MouseButtonEventArgs e) { Debug.WriteLine(«ButtonTwo RightButtonDown»); } void GroupBoxPreviewRightButtonDown(object sender, MouseButtonEventArgs e) { Debug.WriteLine(«GroupBox PreviewRightButtonDown»); } void GroupBoxRightButtonDown(object sender, MouseButtonEventArgs e) { Debug.WriteLine(«GroupBox RightButtonDown»); } void WindowPreviewRightButtonDown(object sender, MouseButtonEventArgs e) { Debug.WriteLine(«Window PreviewRightButtonDown»); } void WindowRightButtonDown(object sender, MouseButtonEventArgs e) { Debug.WriteLine(«Window RightButtonDown»); } Во время работы этой программы события возникают в следующем порядке: 1. Window PreviewMouseRightButtonDown. 2. Window PreviewMouseRightButtonDown. 3. GroupBox PreviewMouseRightButtonDown. 4. Button PreviewMouseRightButtonDown. 5. Button MouseRightButtonDown. 6. GroupBox MouseRightButtonDown. 7. Window MouseRightButtonDown. В качестве еще одной иллюстрации на рис. 7.3 показаны две фазы маршрутизации событий. Любое поведение элемента управления по умолчанию должно быть реали зовано во всплывающей версии события. Например, элемент Button создает событие Click в обработчике события MouseLeftButtonUp. Этот принцип неиспользования Previewсобытий позволяет разработчикам приложений задействовать последние для вставки собственной логики или отмены принятого по умолчанию поведения элемен та. В любой точке процедуры мы можем присвоить значение true свойству Handled аргумента события и тем самым предотвратить вызов последующих обработчиков. Фаза всплытия Window Window всп лы 6 ие Button всп ван 3 5 ие лыт GroupBox иро 2 нел GroupBox тун тун нел ир ова ни 1 тие е Фаза туннелирования 4 Button Рис. 7.3. Туннелирование и всплытие событий в дереве элементов События 351 Чтобы увидеть, как этот механизм работает, подпишемся на событие PreviewMouseLeftButtonDown объекта Window. Присвоив свойству Handled значение true, мы сможем проигнорировать щелчок по любому элементу: public Window1() { ... this.PreviewMouseRightButtonDown += WindowPreviewRightButtonDown; } void WindowPreviewRightButtonDown(object sender, MouseButtonEventArgs e) { e.Handled = true; } Handled – одно из нескольких интересных свойств, общих для всех маршру тизируемых событий. Аргумент, передаваемый при возникновении любого тако го события, – экземпляр класса, производного от RoutedEventArgs: public class RoutedEventArgs : EventArgs { public bool Handled { get; set; } public object OriginalSource { get; } public RoutedEvent RoutedEvent { get; set; } public object Source { get; set; } } Свойства OriginalSource и Source оказываются весьма полезны. Source ссыла ется на отправителя события, то есть на объект, к которому мы присоединили об работчик. А OriginalSource – это объект, в котором возникло событие, то есть ис тинный источник. В предыдущих примерах истинным источником был объект ButtonChrome – визуальный элемент, включенный в шаблон кнопки. Поскольку события мыши относятся к числу физических, их истинным источником всегда является элемент, над которым находилась мышь в момент возникновения собы тия. Истинным же источником большинства семантических событий, того же Click, будет элемент, создавший событие. Чтобы это стало очевидно, напишем код, который будет реагировать на события нажатия и отпускания кнопки мыши, а также на щелчки. При той же структуре де рева элементов, что и выше, мы получим следующую последовательность событий: Window PreviewMouseLeftButtonDown,Source=Button,Original=ButtonChrome GroupBox PreviewMouseLeftButtonDown,Source=Button,Original=ButtonChrome ButtonTwo PreviewMouseLeftButtonDown,Source=Button,Original=ButtonChrome ButtonTwo Click, Source=Button, OriginalSource=Button GroupBox Click, Source=Button, OriginalSource=Button Window Click, Source=Button, OriginalSource=Button Обратите внимание, что событие MouseLeftButtonDown отсутствует. Связано это с тем, что элемент Button сам обрабатывает его, чтобы впоследствии возбу дить событие Click. 352 Глава 7. Действия Туннелирование и всплытие прекрасно работают для таких встроенных в каждый элемент управления событий, как события мыши. Однако туннелироваться и всплы вать может любое событие (как показывает предыдущий пример с Click).Чтобы обеспечить и туннелирование, и всплытие, WPF поддерживает присоединенные со бытия. Подобно тому, как система свойств позволяет присоединять свойства к любо му элементу, мы можем присоединить к любому элементу и обработчик событий. Если мы хотим получать извещения о нажатии любой кнопки в окне, доста точно просто вызвать метод AddHandler. У каждого события в WPF имеется свойство типа RoutedEvent, которое во время исполнения ссылается на объект источник. Чтобы присоединить обработчик, мы передаем методу AddHandler объект RoutedEvent и делегат, который необходимо вызвать: this.AddHandler(Button.ClickEvent, (RoutedEventHandler)delegate { MessageBox.Show(«Clicked»); }); WPF расширяет базовую модель событий .NET, добавляя систему маршрути зации событий, которая учитывает композицию элементов. Все остальные сред ства обработки действий построены на базе этой модели маршрутизации. Команды Большинство событий в WPF связаны с деталями реализации конкретных эле ментов управления: изменился выбор, произошел щелчок, передвинулась мышь и т.д. События хороши, когда нужно выполнить некоторый код в ответ на получение изве щения от элемента управления, но часто бывает нужен более абстрактный подход. Предположим, что нужно реализовать возможность завершения программы (Уж это большинство программ обязаны поддерживать!). Конечно, необходимо включить соответствующий пункт в меню. Поэтому мы первым делом определим в разметке меню: <MenuItem Header=’_File’> <MenuItem Header=’E_xit’ Click=’ExitClicked’ /> </MenuItem> В файле с кодом реализуем обработчик события: void ExitClicked(object sender, RoutedEventArgs e) { Application.Current.Shutdown(); } Пока все хорошо, но давайте еще добавим текст, в который входит гиперссыл ка, позволящая выйти из программы: <TextBlock> Вас приветствует моя программа. Если вам надоело, можете <Hyperlink Click=’ExitClicked’>выйти</Hyperlink>. </TextBlock> Команды 353 Вот теперь начинаются неприятности. Мы делаем слишком много предполо жений о реализации метода ExitClicked, например, что его сигнатура совместима с событием Hyperlink.Click и что он не делает ничего другого, кроме завершения приложения. К тому же, в разметку оказались зашиты произвольно выбранные имена методов из файла с кодом, а дизайнер, который конструирует пользова тельский интерфейс, не будет знать, к каким обработчикам событий привязаться. Для решения этой проблемы и придуманы команды. Они позволяют назна чить имя желаемому действию. Чтобы воспользоваться командой, нужно сделать три вещи: (1) определить назначение команды; (2) написать реализацию коман ды и (3) создать для команды триггер Основой всех команд в WPF является довольно простой интерфейс ICommand: public interface ICommand { event EventHandler CanExecuteChanged; bool CanExecute(object parameter); void Execute(object parameter); } Метод CanExecute позволяет выяснить, находится ли команда в таком состоя нии, когда ее можно выполнить. Обычно элементы управления пользуются этим методом, чтобы активировать или деактивировать себя. Иными словами, если ассо циированная с кнопкой команда возвращает false из метода CanExecute, то кнопка деактивируется. Такое обобществление понятия «активен» позволяет нескольким элементам, связанным с одной командой, поддерживать согласованное состояние. Метод Execute основной, его вызов означает выполнение команды. Реализа ция класса Button (как и любого другого элемента управления, поддерживающе го команды) должна включать примерно такой код: protected virtual void OnClick(RoutedEventArgs e) { if (Command != null && Command.CanExecute(CommandParameter)) { Command.Execute(CommandParameter); } // ... продолжение реализации } Для определения новой команды мы должны реализовать интерфейс ICommand. Поскольку мы хотим, чтобы наша команда закрывала приложение, то можем вызвать метод Shutdown: public class Exit : ICommand { public bool CanExecute(object parameter) { return true; } public event EventHandler CanExecuteChanged; Глава 7. Действия 354 public void Execute(object parameter) { Application.Current.Shutdown(); } } Для привязки команды к пункту меню или к ссылке мы указываем в свойстве Command имя команды – Exit: <MenuItem Header=’_File’> <MenuItem Header=’E_xit’> <MenuItem.Command> <l:Exit /> </MenuItem.Command> </MenuItem> </MenuItem> ... <Hyperlink> <Hyperlink.Command><l:Exit /></Hyperlink.Command> ... </Hyperlink> Так как команда часто вызывается из нескольких мест, принято заводить ста тическое поле, содержащее экземпляр команды: public partial class Window1 : Window { public static readonly ICommand ExitCommand = new Exit(); ... } Дополнительный плюс такой реализации заключается в том, что реализацию класса Exit можно скрыть, объявив, что поле имеет тип ICommand. Теперь Exit можно сделать закрытым классом, а в разметке привязаться к статическому полю: <MenuItem Header=’_File’> <MenuItem Header=’E_xit’ Command=’{x:Static l:Window1.ExitCommand}’ /> </MenuItem> </MenuItem> Таким образом, наше окно может раскрывать свою функциональность в виде команд. Однако тут имеется интересная проблема. Сейчас команда Exit реализо вана на глобальном уровне; мы можем вызвать ее из любого места и тем самым за вершить приложение. Предположим, однако, что «exit» должно означать закры тие текущего окна. В таком случае хотелось бы отделить реализацию выхода от определения, как это было с событиями. Простейший способ добиться такого разделения состоит в том, чтобы прибег нуть к системе событий. Мы можем определить новое событие, связанное с ко мандой, и воспользоваться маршрутизацией для уведомления компонентов: class Exit : ICommand { Команды 355 public static readonly RoutedEvent ExecuteEvent = EventManager.RegisterRoutedEvent( «Execute», RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Exit)); ... } Поскольку для этого события задана стратегия Bubble, оно будет всплывать от источника. Чтобы возбудить событие, мы изменим реализацию метода Execute, так чтобы он искал текущий элемент (в данном примере мы воспользо вались для этой цели методом Keyboard.FocusedElement, но могли бы остано виться на любом механизме обнаружения «текущего»), а затем возбуждал под ходящее событие: public void Execute(object parameter) { RoutedEventArgs e = new RoutedEventArgs(Exit.ExecuteEvent, Keyboard.FocusedElement); Keyboard.FocusedElement.RaiseEvent(e); } Это подводит нас к идее привязки команд – возможности отделить реализа цию команды от ее назначения. Вернемся к классу Window1 и добавим в него ре ализацию команды: public partial class Window1 : Window { ... public Window1() { InitializeComponent(); AddHandler(Exit.ExecuteEvent, ExitExecuted); } void ExitExecuted(object sender, RoutedEventArgs e) { this.Close(); } } Тут возможно некоторое недопонимание. Напомним, что цель команды – предложить абстракцию того, что должно происходить. В данном случае элемент управления (скажем, MenuItem) вызывает команду в ответ на событие (к приме ру, Click). При нашей реализации команда Exit возбудит событие Execute, кото рое распространится по дереву элементов, а объект Window сможет подписаться на него и выполнить те или иные действия (рис. 7.4): 1. Пользователь щелкает по пункту меню. 2. MenuItem вызывает метод Execute команды. 3. Реализация команды Exit возбуждает событие Exit.Execute от имени эле Глава 7. Действия 356 мента, имеющего фокус (в данном случае MenuItem). 4. Событие всплывает вверх по дереву. 5. Window получает событие Exit.Execute. 6. Window выполняет обработчик события (закрывает окно). 6 Window Command Bu le bb 5 Bu b bl e ExitExecuted! 4 Menultem C 1 2 E lic k! Hyperlink e ut 3 it! x E c xe Рис. 7.4. Поток выполнения команды, включающий маршрутизируемое событие Можно было бы пойти дальше и включить в интерфейс ICommand средства поддержки привязок к вводу (для обработки ввода с клавиатуры, от мыши и пе ра), параметров и прочего2. Однако в каркасе есть встроенный класс RoutedCommand, который большую часть всего этого уже умеет делать. Маршрутизируемые команды позволяют полностью отделить реализацию ко манды от ее назначения. Паттерн определения новой команды похож на RoutedEvent и DependencyProperty. Команда определяется как статическое свойство; это просто уникальный маркер, обозначающий ее идентичность: public partial class Window1 : Window { public static readonly ICommand ExitCommand = new RoutedCommand(«Exit», typeof(Window1)); ... } Чтобы при выполнении команды чтото происходило, нужно связать с коман дой обрабатывающий код (аналогично тому, как мы поступили с событием в пре дыдущем примере). Маршрутизируемые команды всплывают (как и события), поэтому можно добавить привязку к команде в корневое окно и таким образом увидеть все команды. Для привязки к команде нужно указать вид интересующей команды и код, который будет выполняться при ее поступлении: public Window1() { InitializeComponent(); CommandBindings.Add(new CommandBinding(ExitCommand, ExitExecuted)); } 2 Общее обоснование для создания специализированного варианта маршрутизации команд заключается в том, что маршрут не обязательно привязан к дереву элементов. Нетрудно представить систему со структурой, характерной для документов и проектов, в которой ко% манда должна проходить по логическому пути, а не по дереву элементов. Команды 357 void ExitExecuted(object sender, ExecutedRoutedEventArgs e) { this.Close(); } Привязка к команде позволяет решить, следует ли активировать команду, а также с помощью свойства InputBindings отобразить на команды действия3 по вводу данных (жесты): <Window x:Class=’EssentialWPF.Window1’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ xmlns:l=’clrnamespace:EssentialWPF’ Title=’EssentialWPF’ > <Window.InputBindings> <KeyBinding Key=’A’ Modifiers=’Control’ Command=’{x:Static l:Window1.ExitCommand}’ /> </Window.InputBindings> ... </Window> Еще одна существенная особенность – это понятие о «безопасных командах». С некоторыми командами, например вырезания, копирования и вставки, сопря жены определенные угрозы. С целью гарантировать, что система выполняет та кие операции только по явному запросу пользователя (или если разрешает сис тема управления цифровыми правами), класс RoutedCommand может отслежи вать, была ли команда инициирована пользователем. Использование команд четко отделяет отображение от поведения. Заведя един ственное имя, обозначающее семантическое действие, мы обходим многие проблемы тесной связи, которые могут возникнуть при попытке ассоциировать несколько эле ментов управления с одним обработчиком события. В общем случае логику приложе ния следует реализовывать в терминах команд, а не обработчиков событий. Во многих типичных ситуациях, когда принято устанавливать прямую ассоциацию с обработчи ком события, лучше воспользоваться триггерами. О них мы расскажем чуть ниже. Команды и привязка к данным Одна из наиболее интересных и мощных возможностей команд – это интегра ция с привязкой к данным. Поскольку у элементов есть свойства Command и CommandParameter, их можно привязать к данным. А, значит, именно от данных будет зависеть происходящее в программе. В главе 6 мы говорили о пользова тельских интерфейсах, управляемых данными. Так вот, команды позволяют реа лизовать и логику, управляемую данными. Чтобы понять, как все стыкуется, напишем приложение, которое выводит спи сок всех файлов на диске c:\. Определим простое диалоговое окно со списковым полем и шаблон данных для отображения имени одного файла: 3 К жестам относятся перемещения мыши, нажатия клавиш и ввод от пера. Подробно они рас% смотрены в приложении. Глава 7. Действия 358 <Window x:Class=’EssentialWPF.DataAndCommands’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’Data and Commands’ > <ListBox Margin=’2’ Name=’_files’> <ListBox.ItemTemplate> <DataTemplate> <TextBlock Text=’{Binding Path=Name}’ /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Window> Затем запишем в свойство ItemsSource список файлов: public partial class DataAndCommands : Window { public DataAndCommands() { InitializeComponent(); FileInfo[] fileList = new DirectoryInfo(«c:\\»).GetFiles(«*.*»); _files.ItemsSource = fileList; } } Эта программа выводит окно, показанное на рис. 7.5. Отлично! Теперь хотелось бы добавить кнопку для вывода содержимого фай ла. Но нам не нужно, чтобы запускались произвольные приложения, поэтому следует поставить фильтр на те типы файлов, которые мы согласны загружать. У нас будет две команды: Open и Blocked. Рис. 7.5. Отображение списка файлов public partial class DataAndCommands : Window { public static readonly RoutedCommand OpenCommand = new RoutedCommand(«Open», typeof(DataAndCommands)); public static readonly RoutedCommand BlockedCommand = new RoutedCommand(«Blocked», typeof(DataAndCommands)); Команды 359 ... } Понадобятся также обработчики этих команд: public partial class DataAndCommands : Window { public DataAndCommands() { InitializeComponent(); CommandBindings.Add(new CommandBinding(OpenCommand, delegate (object sender, ExecutedRoutedEventArgs e) { Process.Start(«notepad.exe», (string)e.Parameter); })); CommandBindings.Add(new CommandBinding(BlockedCommand, delegate (object sender, ExecutedRoutedEventArgs e) { MessageBox.Show((string)e.Parameter, «Blocked»); })); } } Определив обе команды, мы можем модифицировать шаблон данных, вклю чив в него кнопку. Можно воспользоваться привязкой к данным для задания па раметра команды (имени файла). Что касается самой команды, то, поскольку мы хотим, чтобы для одних элементов списка вызывалась команда OpenCommand, а для других – BlockedCommand, то реализуем интерфейс IValueConverter для пе рехода от имени файла к объекту ICommand: <DataTemplate> <WrapPanel> <TextBlock Text=’{Binding Path=Name}’ /> <Button CommandParameter=’{Binding Path=FullName}’> <Button.Command> <Binding> <Binding.Converter> <l:FileToCommandConverter /> </Binding.Converter> </Binding> </Button.Command> Show </Button> </WrapPanel> </DataTemplate> Конвертер может проанализировать данные и решить, какую команду выпол нять. Например, можно проверить расширение файла: public class FileToCommandConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { Глава 7. Действия 360 string ext = ((FileInfo)value).Extension.ToLowerInvariant(); if (ext == «.txt») { return DataAndCommands.OpenCommand; } return DataAndCommands.BlockedCommand; } ... } Запустив эту программу (рис. 7.6), мы убедимся, что можно просматривать со держимое только текстовых файлов4. Этот пример слишком прост; того же само го можно было бы достичь с помощью метода CanExecute с последующим блоки рованием выполнения команды для «плохих» файлов. Но важно подчеркнуть, что мы могли бы вернуть любую команду. Таким образом, поведение элемента определяется данными. Рис. 7.6. Результат работы программы с командами, которые привязаны к данным Команды обеспечивают слабую связь между пользовательским интерфей сом и поведением. Кроме того, команды открывают возможность подойти к определению поведения приложения на основе данных. Часть поведения от носится не к логике самого приложения, а лишь к манипуляциям над визуаль ным отображением его состояния. Например, когда пользователь проводит мышью над кнопкой, кнопка должна подсвечиваться. Такую логику легко ре 4 Не надо думать, что это безопасный способ реализовать фильтрацию. Любую фильтрацию, основанную только на расширении файла, легко обмануть. Я лишь хотел продемонстрировать простой пример, не пытаясь проанализировать само содержимое или сделать еще что%то действительно безопасное. Триггеры 361 ализовать с помощью команд или обработчиков событий, но, коль скоро это поведение перенесено в код, управлять им с помощью инструментальных средств становится очень трудно, и, следовательно, мы вновь приходим к тес ной связи между отображением и поведением. Именно для решения этой проблемы и предназначены триггеры. Триггеры В главе 5 мы впервые познакомились с триггерами, когда воспользовались ими в сочетании с шаблоном элемента управления для декларативного за пуска анимации. Триггер может сработать по одному из трех условий: (1) из менение состояния свойства отображения (Trigger); (2) изменение состоя ние свойства данных (DataTrigger); (3) событие (EventTrigger). Все три типа триггеров при срабатывании запускают некоторую последовательность действий. Существует также два типа триггеров для наборов: MultiTrigger и MultiDataTrigger. Триггерами можно пользоваться только внутри шаблона или стиля5. Объекты Trigger и EventTrigger допустимы внутри шаблона элемента управления или сти ля, а объекты DataTrigger – только внутри шаблона данных. Добавление триггеров к данным В шаблоне данных есть два способа связать элемент отображения с частью мо дели данных. Мы уже видели, как это можно сделать с помощью привязки, при чем дополнительно можно еще воспользоваться конвертером для преобразова ния значения из модели данных в нечто понятное элементам отображения. Класс DataTrigger дает декларативный способ задать действия, которые сле дует выполнить для указанных значений из модели данных. Тем самым DataTrigger – это, по сути дела, простой конвертер значений, определенный в разметке. С помощью привязки он получает значение из модели данных, а когда это значение соответствует заранее заданному, использует последовательность объектов Setter и EventSetter. Для демонстрации возьмем последний пример из раздела, посвященного командам (рис. 7.6), и переделаем конвертер значений в DataTrigger. Вместо того чтобы пользоваться конвертером в привязке к свойству кнопки Command, мы привяжем к нему значение по умолчанию (в данном случае BlockedCommand): <DataTemplate> <WrapPanel> <TextBlock Text=’{Binding Path=Name}’ /> <Button 5 Пусть вас не вводят в заблуждение свойства Triggers в классах FrameworkElement и FrameworkContentElement. Они аналогичны аппендиксу в человеческом теле: пользы мало и мож% но удалить, когда из%за него возникают проблемы. А если серьезно, триггеры реализованы на ба% зе системы свойств и работают только внутри шаблонов и стилей. Локальное свойство Triggers по% ка просто резервирует место и, надо надеяться, еще покажет себя в последующих версиях WPF. 362 Глава 7. Действия Command=’{x:Static l:DataAndTriggers.BlockedCommand}’ CommandParameter=’{Binding Path=FullName}’> Show </Button> </WrapPanel> </DataTemplate> Далее следует описать триггер данных, который будет срабатывать, когда файл имеет расширение «.txt»: <DataTemplate> <WrapPanel> <TextBlock Text=’{Binding Path=Name}’ /> <Button Command=’{x:Static l:DataAndTriggers.BlockedCommand}’ CommandParameter=’{Binding Path=FullName}’> Show </Button> </WrapPanel> <DataTemplate.Triggers> <DataTrigger Binding=’{Binding Path=Extension}’ Value=’.txt’> </DataTrigger> </DataTemplate.Triggers> </DataTemplate> Если расширение равно «.txt», то свойству Command должно быть присвоено значение OpenCommand. Для этого нужно включить в триггер объект Setter, ко торый и запишет нужное значение в свойство. В данном случае нужно еще задать объект, которому принадлежит свойство, поскольку мы не хотим изменять свой ства в модели данных: <DataTemplate> <WrapPanel> <TextBlock Text=’{Binding Path=Name}’ /> <Button x:Name=’_showButton’ Command=’{x:Static l:DataAndTriggers.BlockedCommand}’ CommandParameter=’{Binding Path=FullName}’> Show </Button> </WrapPanel> <DataTemplate.Triggers> <DataTrigger Binding=’{Binding Path=Extension}’ Value=’.txt’> <Setter TargetName=’_showButton’ Property=’Command’ Value=’{x:Static l:DataAndTriggers.OpenCommand}’ /> </DataTrigger> </DataTemplate.Triggers> </DataTemplate> Триггеры 363 Рис. 7.7. Применение DataTrigger вместо конвертера значений и установка сразу нескольких свойств Поскольку DataTrigger может содержать более одного объекта Setter, то легко организовать выполнение нескольких действий, когда элемент данных примет интересующее нас значение. Добавим для примера еще один объект, который бу дет показывать подлежащую выполнению команду (рис. 7.7). Определенный по рядок вызова установщиков не гарантируется, хотя в текущей версии они приме няются в порядке объявления. <DataTemplate> <WrapPanel> <TextBlock Text=’{Binding Path=Name}’ /> <Button x:Name=’_showButton’ Command=’{x:Static l:DataAndTriggers.BlockedCommand}’ CommandParameter=’{Binding Path=FullName}’ Content=’Block’ /> </WrapPanel> <DataTemplate.Triggers> <DataTrigger Binding=’{Binding Path=Extension}’ Value=’.txt’> <Setter TargetName=’_showButton’ Property=’Command’ Value=’{x:Static l:DataAndTriggers.OpenCommand}’ /> <Setter TargetName=’_showButton’ Property=’Content’ Value=’Show’ /> </DataTrigger> </DataTemplate.Triggers> </DataTemplate> При работе с классом DataTrigger мы можем только сравнивать значения на равенство. Предыдущий пример отличается от варианта с использованием кон вертера значений в одной существенной детали: в первоначальной версии для об Глава 7. Действия 364 работки возможных различий в регистре букв в имени файла вызывался метод ToLowerInvariant. Чтобы сохранить эту возможность, мы можем создать простой конвертер значений для приведения к нижнему регистру: public class ToLowerInvariantConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return ((string)value).ToLowerInvariant(); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return value; } } Подключить конвертер к триггеру очень просто: <DataTrigger Value=’.txt’> <DataTrigger.Binding> <Binding Path=’Extension’> <Binding.Converter> <l:ToLowerInvariantConverter /> </Binding.Converter> </Binding> </DataTrigger.Binding> <Setter TargetName=’_showButton’ Property=’Command’ Value=’{x:Static l:DataAndTriggers.OpenCommand}’ /> <Setter TargetName=’_showButton’ Property=’Content’ Value=’Show’ /> </DataTrigger> С помощью DataTrigger мы можем перенести всю зависящую от пользова тельского интерфейса логику (привязку нужной команды) в разметку, оставив в коде лишь самый минимум (преобразование строк). Перенос логики в разметку упрощает создание качественных инструментов для отображения и четко отделя ет интерфейс от логики приложения. Добавление триггеров к элементам управления Если триггеры данных расширяют возможности по выносу логики из конвер теров значений, то при проектировании классов Trigger и EventTrigger нами ру ководило желание вынести в разметку всю зависящую от представления логику элемента управления. Цель класса ControlTemplate – позволить полностью изме нять внешний вид элемента, не меняя его логику: Триггеры 365 <ControlTemplate TargetType=’{x:Type Button}’> <Border x:Name=’border’> <ContentPresenter /> </Border> <ControlTemplate.Triggers> <Trigger Property=’IsPressed’ Value=’true’> <Setter TargetName=’border’ Property=’Background’ Value=’Red’ /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> DataTrigger тут помогает только частично: мы можем декларативно выпол нить преобразование данных и привязку. Но для отображения элемента управле ния нужна большая гибкость. Класс Trigger дополняет функциональность DataTrigger, добавляя свойства EnterActions и ExitActions. Следовательно, в от вет на переход свойства из одного состояния в другое мы можем, например, начи нать и заканчивать анимацию (как было показано в главе 5). С другой стороны, объект EventTrigger получает извещение от события (к при меру, MouseEnter или Loaded) и позволяет управлять несколькими анимациями. Сейчас мы можем сформулировать два интересных наблюдения, касающихся триггеров. Вопервых, в ответ на срабатывание триггера могут выполняться «действия» двух видов: управляющие действия в раскадровке и объекты Setter. Объекты Setter поддерживают настройку свойств и событий, а управляющие действия в раскадровке позволяют выполнять воспроизведение, останов, паузу и т.д. Объекты EventTrigger, а также действия при входе и выходе, заданные в объ екте Trigger, работают только с управляющими действиями в раскадровке. Второе любопытное наблюдение связано с тем, что список триггеров и иници ируемых ими действий в версии WPF 1.0 не расширяем. Однако раздвинуть гра ницы применимости триггеров можно довольно широко за счет использования сложных триггеров и их комбинирования. Триггеры как новый вариант if Триггеры представляют собой простой механизм задания правил. Они позво ляют выразить некое условие (с помощью значения) и действие (список объек тов Setter или действий при входе и выходе). В терминах языка C# триггеры эк вивалентны примерно такому коду: if (IsPressed) { border.Background = Brushes.Red; } Поскольку классы MultiTrigger и MultiDataTrigger позволяют задавать нес колько условий, то с помощью триггеров можно реализовать и аналог такого кода: if (IsPressed && IsMouseOver) { border.Background = Brushes.Red; } 366 Глава 7. Действия Для этого потребуется MultiTrigger (в предположении, что шаблон элемента управления остался без изменения): <ControlTemplate TargetType=’{x:Type Button}’> <Border x:Name=’border’> <ContentPresenter /> </Border> <ControlTemplate.Triggers> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property=’IsPressed’ Value=’true’ /> <Condition Property=’IsMouseOver’ Value=’true’ /> </MultiTrigger.Conditions> <Setter TargetName=’border’ Property=’Background’ Value=’Red’ /> </MultiTrigger> </ControlTemplate.Triggers> </ControlTemplate> Если ни один триггер не сработает, то будет взято значение, заданное в самом элементе. На деле это означает, что ветвь else предложения if находится внутри элемента. В данном случае мы можем оставить фон синим, если триггеры не при меняются: <ControlTemplate TargetType=’{x:Type Button}’> <Border x:Name=’border’ Background=’Blue’> <ContentPresenter /> </Border> <ControlTemplate.Triggers> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property=’IsPressed’ Value=’true’ /> <Condition Property=’IsMouseOver’ Value=’true’ /> </MultiTrigger.Conditions> <Setter TargetName=’border’ Property=’Background’ Value=’Red’ /> </MultiTrigger> </ControlTemplate.Triggers> </ControlTemplate> Чего мы достигли? В этой главе мы рассмотрели, как добавить поведение в ответ на событие, ини циированное пользователем или системой, с помощью различных моделей действий: событий, команд и триггеров. 367 Глава 8. Стили В предшествующих шести главах мы рассмотрели все части, из которых конструируется приложение на платформе Windows Presentation Foundation. Мы видели, как оно составляется из элементов управления, как эти элементы по зиционируются с помощью менеджеров размещения, как они визуализируются и интегрируются с данными и, наконец, как выполняются действия. Последнее, что осталось, – это стили. Стиль – это набор свойств и действий, ассоциируемых с элементом. Стиль аг регирует все виды настроек: изменение шаблонов, анимацию, привязки к дан ным, команды и прочие средства, предоставляемые WPF. Стили можно комби нировать с другими ресурсами и упаковывать в тему. Темы открывают возмож ность взаимодействия между дизайнерами и программистами. С помощью тем приложению можно придать совершенно новый облик. Принципы стилизации Идея создания отдельного определения стиля или темы витает в воздухе уже давно. Впервые со стилями и темами я познакомился много лет назад, работая с Microsoft Word. В Word есть именованные контейнеры информации о формати ровании: Заголовок 1, Заголовок 2, Обычный и т.д. Первая попытка применить стили в мире Web была встроена уже в базовую семантическую разметку, это те ги типа H1, H2 и EMPHASIS. Такие семантические теги браузер мог представ лять в том или ином стиле. С появлением каскадных таблиц стилей (CSS) у ав тора документа появилась возможность определять свой стиль для любого тега с помощью специального языка. В WPF мы с самого начала хотели поддержать какойто способ стилизации, то есть возможность повторно использовать стилевую информацию для нескольких элементов. Это обеспечило бы единообразие пользовательского интерфейса и подде ржку совместной работы дизайнеров и программистов. На долю дизайнера выпада ло бы определение стиля, а программист занимался бы структурой интерфейса. Кри тически важным представлялось наличие единой модели, охватывающей ГИП, доку менты и мультимедиа. При разработке системы стилей в WPF мы руководствова лись тремя принципами: (1) учесть композицию элементов; (2) предоставить унифи цированную модель для настройки; (3) оптимизировать инструментальные средства. Композиция элементов Мы уже видели, что в WPF композиция используется чрезвычайно широко, а это ставит перед системой стилизации ряд интересных вопросов. Вопервых, как Глава 8. Стили 368 следует ассоциировать стили с элементами? Первоначально мы подумывали о том, чтобы задать один стиль в корне дерева элементов, а затем с помощью како гото языка применять стили к элементам на разных уровнях дерева. Чтото типа приведенного ниже фрагмента: <Window ...> <Window.Styles> <!— применить этот стиль к любой кнопке Button внутри StackPanel... (конечно, это не настоящий синтаксис). —> <Style ApplyTo=’StackPanel Button’> ... </Style> </Window.Styles> </Window> Однако такая модель была не слишком пригодна в случае композиции эле ментов и глубоких иерархий. Тот факт, что в атрибуте ApplyTo кодируется струк тура документа, нарушает инкапсуляцию. К тому же, если уровень вложенности велик, то подобные выражения становятся чрезмерно громоздкими. На самом деле хорошо было бы иметь возможность задавать стиль для любого контейнерного элемента, тогда можно было бы обойтись без ApplyTo. Кроме того, мы хотели располагать определения стилей как можно ближе к самим элементам; если некоторый набор стилей применяется только к одному участку дерева отоб ражения, то вряд ли имеет смысл задавать область действия для каждого правила. Поэтому вместо идеи глобальных стилей мы решили определять область действия для каждого стиля. Следовательно, новый стиль можно определить в любой точке дерева: <Window ...> <Window.Style> <Style>...</Style> </Window.Style> <StackPanel> <StackPanel.Style> <Style>...</Style> </StackPanel.Style> </StackPanel> </Window> Кроме того, ссылки на ресурсы работают так, что мы можем воспользоваться системой ресурсов для определения области действия именованных и типизиро ванных стилей в дереве: <Window ...> <Window.Resources> <Style x:Key=’someStyle’>...</Style> </Window.Resources> <StackPanel> <StackPanel.Resources> Принципы стилизации 369 <Style x:Key=’someStyle’>...</Style> </StackPanel.Resources> <!— Все находящееся внутри StackPanel получит стиль «someStyle» от StackPanel, а не от Window. —> </StackPanel> </Window> Поскольку все в WPF так сильно завязано на композицию элементов, систе ма стилей также должна быть оптимизирована с учетом композиции. Помимо ба зовой композиции нашей целью было уницифировать работу с пользовательским интерфейсом, документами и мультимедиа. Следовательно, и стилизация долж на применяться унифицированно. Унифицированная модель настраивания В WPF унификация понимается двояко: (1) как возможность применять стили к любому презентационному домену (ГИП, документам и мультимедиа); (2) как воз можность настраивать все в системе (свойства, действия, отображение и т.д.). Одно из существенных преимуществ унифицированной презентационной платформы сос тоит в том, что возможность использовать стили повсеместно ничего не стоит при ус ловии, что система стилизации дает доступ ко всем платформенным средствам. Рис. 8.1. Использование методов установки свойств для применения стилей к ГИП, документам и мультимедиа Нам нужно было, чтобы стили умели делать всего две вещи: устанавливать свойства и получать извещения о событиях. Если бы мы смогли поддержать толь ко эти две функции, все остальное в системе стало бы доступно. В примере ниже строится набор стилей, применимых к элементам любого ти па: абзацам, кнопкам, фигурам и т.д. (рис. 8.1): <FlowDocumentScrollViewer xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ > <FlowDocument> <FlowDocument.Resources> <Style TargetType=’{x:Type Paragraph}’> <Setter Property=’FontFamily’ Value=’Georgia’ /> 370 Глава 8. Стили <Setter Property=’Margin’ Value=’3’ /> </Style> <Style TargetType=’{x:Type Button}’> <Setter Property=’FontFamily’ Value=’Tahoma’ /> <Setter Property=’Margin’ Value=’3’ /> </Style> <Style TargetType=’{x:Type RadioButton}’> <Setter Property=’FontFamily’ Value=’Tahoma’ /> <Setter Property=’Margin’ Value=’3’ /> </Style> <Style TargetType=’{x:Type CheckBox}’> <Setter Property=’FontFamily’ Value=’Tahoma’ /> <Setter Property=’Margin’ Value=’3’ /> </Style> <Style TargetType=’{x:Type Ellipse}’> <Setter Property=’Fill’ Value=’Red’ /> <Setter Property=’Margin’ Value=’3’ /> </Style> <Style TargetType=’{x:Type Rectangle}’> <Setter Property=’Fill’ Value=’Blue’ /> <Setter Property=’Margin’ Value=’3’ /> </Style> </FlowDocument.Resources> <Paragraph>Hello World</Paragraph> <Paragraph> <RadioButton>Some</RadioButton> <Button>UI</Button> <CheckBox>Controls</CheckBox> </Paragraph> <Paragraph>And some interesting shapes: <Ellipse Width=’15’ Height=’15’ /> <Rectangle Width=’15’ Height=’15’ /> </Paragraph> </FlowDocument> </FlowDocumentScrollViewer> Но ничто не бывает так просто, как хочется. Поэтому есть несколько мест, в которых система стилизации раскрывает детали реализации ядра WPF. Напри мер, самый естественный способ применения триггера к элементу состоит в том, чтобы записать свойство Triggers элемента в объект Setter: <Style TargetType=’{x:Type Button}’> <!— не работает —> <Setter Property=’Triggers’> <Setter.Value> <Trigger Property=’IsMouseOver’ Value=’True’> ... </Trigger> </Setter.Value> </Setter> </Style> Принципы стилизации 371 Однако изза некоторых деталей реализации механизма применения тригге ров мы были вынуждены прибегнуть к более специализированной модели, осно ванной на свойстве Style.Triggers: <Style TargetType=’{x:Type Button}’> <Style.Triggers> <Trigger Property=’IsMouseOver’ Value=’True’> ... </Trigger> </Style.Triggers> </Style> Но принцип все же сохраняется, пусть даже объектная модель несколько ар хаична: если чтото вообще можно сделать с элементом, это можно сделать и в стиле. Однако мы надеемся, что большинство программистов не станут сутками копаться в разметке, поэтому поддержка со стороны инструментальных средств абсолютно необходима. Оптимизация для инструментальных средств Обеспечение хорошей поддержки со стороны инструментов – это, наверное, самая трудная задача из всех относящихся к стилям. Способность CSS приме нять несколько правил к одному элементу в соответствии с описанием на специ ализированном языке поражает. В этом языке есть поддержка для объявления приоритетности правил, выбора по положению в иерархии, по имени тега, по идентификатору элемента и т.д. CSS предлагает невероятно мощную, но и очень сложную для инструментов модель. Одна из проблем, связанных с принятым в CSS подходом к выбору стиля на ос нове правил, – это то, что процесс односторонний. Если есть набор правил, то мы можем определить, на какие элементы он воздействует. Но если свойство элемента изменяется, то практически невозможно определить, какой стиль должен «вла деть» этим свойством. Пусть нужно, чтобы визуальный конструктор обновил опре деление стиля, когда мы изменяем значение некоторого свойства, так чтобы новый стиль можно было использовать и для других элементов. Единственный способ ре шить эту задачу при такой сложной системе правил – постулировать некоторые уп рощения; в CSS общепринято идентифицировать стили именами классов. В WPF возможности выбора стиля намеренно ограничены; в любой момент времени на каждый элемент воздействуют ровно два стиля, и лишь один из них может быть настроен программистом, который использует элемент (второй задан по умолчанию автором элемента). Есть только два способа определить, какой стиль использовать: с помощью прямой ссылки из элемента на именованный ре сурс или на основе типа элемента. По существу, это гибрид стилей Word и CSS. Помимо простоты, эта модель обладает и еще одним достоинством: не нужно создавать специальный язык для определения того, на какие элементы может воздействовать стиль. Да и дизайнерам с программистами будет легче понять, на каких элементах отразится конкретное изменения стиля. 372 Глава 8. Стили Последнее важное решение, касающееся инструментальной поддержки, – это способ закодировать определение стиля в разметке. Если вы знакомы с CSS, то, вероятно, помните, что стиль определяется набором пар «имязначение»: Button { Background = Red; FontSize = 24pt; } Это элегантный и компактный синтаксис, но у него есть два недостатка. Пер вый связан, главным образом, с инструментами: механизм синтаксического ана лиза и обработки стилей при такой записи кардинально отличается от всех ос тальных частей системы (где повсеместно применяется язык XML). Это сильно усложняет инструмент. Поскольку в WPF мы отдаем предпочтение XML, то ре шили кодировать стили в нотации XAML: <Style TargetType=’{x:Type Button}’> <Setter Property=’Background’ Value=’Red’ /> <Setter Property=’FontSize’ Value=’24pt’ /> </Style> Вторая проблема касается композиции: в нотации CSS нет удобного способа организовать иерархию сложных значений. А на XAML мы можем определить произвольно сложные конструкции: <Style TargetType=’{x:Type Button}’> <Setter Property=’Background’> <Setter.Value> <LinearGradientBrush ...> ... </LinearGradientBrush> </Setter.Value> </Setter> <Setter Property=’FontSize’ Value=’24pt’ /> </Style> Уяснив принципы композиции элементов, унифицированной стилизации и оптимизации для инструментов, мы можем перейти к деталям работы системы стилизации. Введение в стили Стиль состоит из трех компонентов: установщиков свойств, триггеров и ре сурсов. Мы начнем с простой кнопки, у которой есть локальное свойство, позво ляющее сделать фон красным: <Button Background=’Red’> Hello, Red World! </Button> Введение в стили 373 Попробуем перенести это свойство в стиль. При создании нового стиля мы должны сообщить (с помощью свойства TargetType), к объектам какого типа он будет применяться: <Style TargetType=’{x:Type Button}’> </Style> Чтобы сделать фон красным, нужно определить установщик свойства с по мощью объекта Setter. Установщики бывают двух типов: для свойств и для собы тий. Установщики свойств позволяют задать значение свойства для всех объек тов, с которыми ассоциирован данный стиль. Установщики событий связывают обработчик событий с объектами, к которым применяется стиль. В общем случае для ассоциирования поведения с элементами управления лучше пользоваться командами, поэтому установщики событий встречаются редко. Имя свойства относится к целевому типу, определенному в стиле; таким обра зом, имя «Background» означает Button.BackgroundProperty: <Style TargetType=’{x:Type Button}’> <Setter Property=’Background’ Value=’Red’ /> </Style> При проектировании модели стилей мы много спорили о том, нужно ли раз рабатывать особый синтаксис для работы с установщиками свойств и событий. Была идея ввести такой же синтаксис, как «в момент использования», то есть нечто такое: <Style> <!— не работает! —> <Button Background=’Red’ /> </Style> Напомним, что один из постулатов WPF гласит: разметка и объектная модель должны быть максимально похожи. В данном случае стандартные правила инте рпретации разметки означали бы, что нужно создать новую кнопку и задать для нее красный фон. Но очевидно, что от стиля мы ожидаем совсем не этого. По су ществу, стиль – это сценарий настройки. Он позволяет настраивать свойства, со бытия, триггеры и ресурсы для объекта. Применяя в стилях синтаксис установщиков, мы ясно говорим в разметке, что это именно сценарий настройки и сохраняем полную синхронизацию с объектной моделью. Чтобы ассоциировать стиль с типом, нужно установить свойство Style: <Button> <Button.Style> <Style TargetType=’{x:Type Button}’> <Setter Property=’Background’ Value=’Red’ /> </Style> </Button.Style> 374 Глава 8. Стили Hello, Red World! </Button> В главе 6 мы видели, что можно воспользоваться ссылкой на ресурс для обоб ществления определения стиля: <Window ...> <Window.Resources> <Style x:Key=’myStyle’ TargetType=’{x:Type Button}’> <Setter Property=’Background’ Value=’Red’ /> </Style> </Window.Resources> <StackPanel> <Button Style=’{DynamicResource myStyle}’> Hello, Red World! </Button> <Button Style=’{DynamicResource myStyle}’> Another button! </Button> </StackPanel> </Window> В главе 6 мы видели, как можно ассоциировать шаблоны данных с типом, вос пользовавшись свойством DataTemplateKey вместо прямой ссылки на шаблон. То же самое справедливо в отношении стилей, нужно лишь в качестве ключа ука зать тип объекта, с которым ассоциируется стиль. Таким образом, мы сможем ав томатически применить стиль ко всем объектам указанного типа. Если в нашем примере положить ключ равным {x:TypeButton}, то любая кнопка, находящаяся внутри окна, будет красной: <Window ...> <Window.Resources> <Style x:Key=’{x:Type Button}’ TargetType=’{x:Type Button}’> <Setter Property=’Background’ Value=’Red’ /> </Style> </Window.Resources> <Button> Hello, Red World! </Button> </Window> Ключ стиля определяет, как тот будет извлекаться из словаря ресурсов. Целе вой тип стиля говорит, к объектам какого типа нужно применять стиль. Все это может стать источником путаницы, так как часто ключ и целевой тип одинаковы1. Однако раньше мы видели, что иногда бывает необходимо создать несколько именованных стилей для одного и того же целевого типа. 1 Может оказаться даже еще более запутанно: если не задать ключ стиля для поиска в словаре ресурсов, то автоматически используется целевой тип. Я такой трюк не рекомендую, так как читать разметку при этом становится сложнее. Введение в стили 375 Помимо установщиков, мы можем ассоциировать со стилем триггеры и ресур сы. Триггеры работают точно так же, как для шаблонов элементов управления и данных. Изменить цвет фона кнопки можно, присоединив триггер к свойству IsMouseOver: <Style x:Key=’{x:Type Button}’ TargetType=’{x:Type Button}’> <Setter Property=’Background’ Value=’Red’ /> <Style.Triggers> <Trigger Property=’IsMouseOver’ Value=’True’> <Setter Property=’Background’ Value=’Blue’ /> </Trigger> </Style.Triggers> </Style> Возможно, у вас возник вопрос, в чем разница между ControlTemplate.Triggers и Style.Triggers? Напомним, что шаблон элемента управления оказывает влияние на дерево отображения элемента. Когда мы меняем цвет фона кнопки, создается впечатление, что стиль тоже воздействует на дерево отображения, но на самом де ле просто устанавливается свойство кнопки Background, а принятый по умолча нию шаблон в классе Button привязывает это свойство к элементу в дереве отоб ражения. Чтобы понять, в чем различие, добавим еще один триггер для свойства IsPressed: <Style x:Key=’{x:Type Button}’ TargetType=’{x:Type Button}’> <Setter Property=’Background’ Value=’Red’ /> <Style.Triggers> <Trigger Property=’IsMouseOver’ Value=’True’> <Setter Property=’Background’ Value=’Blue’ /> </Trigger> <Trigger Property=’IsPressed’ Value=’True’> <Setter Property=’Background’ Value=’Yellow’ /> </Trigger> </Style.Triggers> </Style> Запустив эту программу, мы обнаружим, что при наведении мыши на кнопку, она не становится желтой. Причина в том, что шаблон Button по умолчанию не привязывает свойство Background ни к какому элементу в де реве отображения в случае, когда кнопка нажата. Чтобы понять, почему, рас смотрим упрощенную версию подразумеваемого для Button шаблона элемен та управления: <ControlTemplate TargetType=’{x:Type Button}’> <Border Background=’{TemplateBinding Background}’ x:Name=’border’> <ContentPresenter /> </Border> <ControlTemplate.Triggers> <Trigger Property=’IsPressed’ Value=’True’> 376 Глава 8. Стили <Setter Property=’Background’ Value=’Red’ TargetName=’border’ /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> Поскольку шаблон по умолчанию для Button удаляет привязку цвета фона (явно делая его красным), то триггер в нашем стиле оказывается бессильным. Та кое поведение обусловлено тем, что шаблоны элементов управления могут ока зывать влияние только на свойства элементов в дереве отображения, а стили – только на свойства элементов управления. Модели, отображение и стили Наличие перекрытия между Style и ControlTemplate подводит нас к вопросу о разделении отображения и поведения. В объектноориентированном проектиро вании есть несколько хорошо известных паттернов, один из которых называется «модельвидконтроллер» (MVC). В нем объекты распределяются по трем кате гориям: (1) модель, определяющая структуры данных; (2) вид, определяющий отображение данных, и (3) контроллер, описывающий взаимодействие между моделью и видом. Хотя WPF, строго говоря, не придерживается этого паттерна, полезно вспомнить о нем при рассмотрении стилей и шаблонов. В классах элементов управления WPF, например Button, определена как мо дель данных, так и модель взаимодействия элемента. Отображение же элемента целиком описывается шаблоном. Кроме того, WPF позволяет передать данные с помощью модели содержимого, которая была рассмотрена в главе 3. При нали чии модели содержимого мы получаем суть паттерна MVC без обычной для него сложности. Поразмышлять о таком разделении данных (модели), шаблона (вида) и уп равления (контроллера) любопытно, потому что стили дают простой способ свя зать их воедино. Элементы управления настраиваются с помощью свойств, уста новку которых поддерживают стили. Шаблоны обеспечивают настройку путем привязок свойств и ресурсов, которые также устанавливаются в стилях. А данные передаются элементам управления через свойства последних, и это тоже описы вается стилями. Стало быть, стили не только участвуют в настройке каждого из трех компонентов, но и объединяют их; с помощью стиля мы можем ассоцииро вать с элементом управления шаблон и источник данных. Теперь можно более внимательно присмотреться к взаимосвязи между стиля ми и шаблонами. Начнем с простого шаблона кнопки: <ControlTemplate x:Key=’simpleButtonTemplate’ TargetType=’{x:Type Button}’> <Border> <ContentPresenter HorizontalAlignment=’Center’ Введение в стили 377 VerticalAlignment=’Center’ /> </Border> </ControlTemplate> На первый взгляд не очевидно, что этот шаблон поддерживает какуюто до полнительную настройку; в нем нет ссылок на свойства элемента. В главе 3 мы видели что объект ContentPresenter неявно привязан к свойству Content шаблон ного элемента. Поэтому мы могли бы установить свойство Content и шаблон эле мента управления и посмотреть, что получится. С помощью TemplateBinding мы можем привязать и другие свойства к шабло ну, о чем шла речь в главе 7: <ControlTemplate x:Key=’simpleButtonTemplate’ TargetType=’{x:Type Button}’> <Border Background=’{TemplateBinding Property=Background}’> <ContentPresenter HorizontalAlignment=’Center’ VerticalAlignment=’Center’ /> </Border> </ControlTemplate> Теперь в шаблоне для кнопки есть параметр, который является свойством са мой кнопки, поэтому мы можем стилизовать кнопку с помощью свойства Background и тем самым заставить стиль повлиять на отображение: <Button Template=’{DynamicResource simpleButtonTemplate}’> <Button.Style> <Style TargetType=’{x:Type Button}’> <Setter Property=’Background’ Value=’Red’ /> </Style> </Button.Style> Hello, Red World! </Button> Такой подход позволяет создать несколько более гибкий шаблон, но наши возможности ограничены настройкой только тех свойств, для которых это было предусмотрено автором элемента (например, Background и Foreground). Если же хочется большего, например задать в шаблоне цвет «подсвеченной» кнопки, то у нас есть три варианта: (1) создать новый элемент с дополнительным свойством; (2) создать присоединенное свойство, применимое к настраиваемому элементу; (3) использовать ресурс с заранее известным именем. Вообще говоря, создавать новый элемент только для того, чтобы поддержать стилизацию, – это перебор. В идеале хотелось бы применять стили и шаблоны к существующим элементам. Создание присоединенных свойств и использование именованных ресурсов – очень похожие решения, но присоединенные свойства видятся скорее средствами «программной» настройки, а не стилизации. В общем 378 Глава 8. Стили случае они более приспособлены для изменения поведения, тогда как именован ные ресурсы естественно работают в стилях. Для определения именованного ресурса нам понадобится уникальный ключ, ко торый мы получим, создав статический экземпляр класса ComponentResourceKey2: public static class SimpleButton { public static readonly ComponentResourceKey RingBrush = new ComponentResourceKey(typeof(SimpleButton), «RingBrush»); } Теперь у нас есть есть глобально уникальный идентификатор нового свойства, который мы можем использовать в шаблоне. В приведенной ниже разметке мы связываем свойство Border.BorderBrush с именованным ресурсом: <ControlTemplate x:Key=’simpleButtonTemplate’ TargetType=’{x:Type Button}’> <Border BorderBrush= ‘{DynamicResource {x:Static l:SimpleButton.RingBrush}}’ BorderThickness=’5’ Background=’{TemplateBinding Property=Background}’> <ContentPresenter HorizontalAlignment=’Center’ VerticalAlignment=’Center’ /> </Border> </ControlTemplate> В стиле для элемента Button мы теперь можем настроить этот ресурс, равно как и любые локальные свойства: <Button Template=’{DynamicResource simpleButtonTemplate}’> <Button.Style> <Style TargetType=’{x:Type Button}’> <Style.Resources> <SolidColorBrush x:Key=’{x:Static l:SimpleButton.RingBrush}’ Color=’Red’ /> </Style.Resources> <Setter Property=’Background’ Value=’Red’ /> </Style> </Button.Style> Hello, World! </Button> 2 Класс ComponentResourceKey здесь не обязателен, но он дает удобный способ создать гло% бально уникальное значение. Комбинация типа и строки определяет уникальное пространство имен (строгий именованный тип CLR) и домен имен (строка). Единственное требование сос% тоит в том, чтобы ключ был уникален и корректно реализовывал методы GetHashCode и Equals. Введение в стили 379 Отметим, что стиль теперь в некотором роде зависит от шаблона, так как он ссылается на именованный ресурс, который окажет воздействие лишь, если в шаблоне задана привязка к нему. Поэтому нередко можно встретить случаи, ког да стиль реализует ассоциацию с шаблоном: <Button> <Button.Style> <Style TargetType=’{x:Type Button}’> <Style.Resources> <SolidColorBrush x:Key=’{x:Static l:SimpleButton.RingBrush}’ Color=’Red’ /> </Style.Resources> <Setter Property=’Background’ Value=’Red’ /> <Setter Property=’Template’ Value=’{DynamicResource simpleButtonTemplate}’ /> </Style> </Button.Style> Hello, World! </Button> Если задуматься обо всем вместе – шаблонах, стилях и ресурсах, то мы прихо дим к выводу, что возможен интересный способ упаковки, который целиком оп ределяет внешний облик элемента управления. Он называется темой. Темы При проектировании WPF мы помнили о темах, однако в основном API ори ентирован на стили и ресурсы. Единственное место, где в объектной модели упо минаются темы, – это атрибут сборки ThemeInfoAttribute. По существу тема представляет собой словарь ресурсов. Вы можете этого и не осознавать, но с этим типом мы уже сталкивались. Классу ResourceDictionary принадлежат все свой ства Resources, которые имеются у элементов управления, шаблонов и стилей. Для создания темы проще всего начать с добавления в проект нового XAML файла и определения в нем объекта ResourceDictionary (в этом примере мы назо вем файл myTheme.xaml): <ResourceDictionary xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ > <Style x:Key=’...’ TargetType=’...’> ... </Style> <ControlTemplate x:Key=’...’ TargetType=’...’> ... </ControlTemplate> <SolidColorBrush x:Key=’...’> ... </SolidColorBrush> 380 Глава 8. Стили ... </ResourceDictionary> Этот новый стиль мы можем применить в любом месте, задав свойство MergedDictionaries объекта ResourceDictionary: <Window xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ > <Window.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source=’myTheme.xaml’ /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Window.Resources> ... </Window> При таком употреблении определение темы включается в стандартную схему иерархического поиска ресурсов. Напомним, что в главе 6 мы говорили, что по иск ресурса производится сначала в дереве элементов, затем в объекте Application и наконец среди системных ресурсов. Когда словарь объединяется с деревом, находящиеся в нем ресурсы становятся доступны в соответствующем узле. С помощью этой техники мы можем активизировать разные темы для раз ных частей пользовательского интерфейса. Поиск стиля в основном производится так же, как поиск ресурса, но с одним заметным исключением: атрибут ThemeInfoAttribute позволяет автору типа со общить системе, где будет находиться тема, принятая для этого типа по умолча нию. При поиске стиля система пользуется умолчанием для темы, если ни в де реве элементов управления, ни в объекте Application, ни среди системных ресур сов для данного типа не удается найти никакой информации. Это соглашение об умолчании позволяет авторам элементов управления создавать для них темы. Чтобы определить новый элемент управления, который будет поддерживать темы, нужно выполнить три шага. Вопервых, для ассоциирования типа с темой нужно задать свойство DefaultStyleKey: public class MyCustomControl : FrameworkElement { public static MyCustomControl() { DefaultStyleKeyProperty.OverrideMetadata( typeof(MyCustomControl), new FrameworkPropertyMetadata(typeof(MyCustomControl))); } } Следующий шаг – задать политику обнаружения темы для сборки с помощью атрибута ThemeInfoAttribute: Введение в стили 381 [assembly: ThemeInfo( ResourceDictionaryLocation.SourceAssembly, ResourceDictionaryLocation.SourceAssembly)] Наконец, нужно создать одну или несколько тем для элемента управления. Каждая тема должна быть добавлена в приложение с правильным именем (см. табл. 8.1) и помечена как страница в файле проекта (напомним, что в главе 2 мы включали в проект компилируемые XAMLфайлы, задавая для них тип Page). Тема Цвет Имя в словаре ресурсов Windows XP Style Синий themes\luna.normalcolor.xaml Оливковый themes\luna.homestead.xaml Серебристый themes\luna.metallic.xaml Windows XP Media Center Edition; Windows XP Tablet PC Edition themes\royale.normalcolor.xaml Windows Vista Style themes\aero.normalcolor.xaml Classic themes\classic.xaml Любая другая (резервный вариант) themes\generic.xaml Таблица 8.1. Темы операционной системы и имена, которые следует использовать в словаре ресурсов Применяя эту технику, мы можем создать элемент управления, который будет выглядеть поразному для каждой темы операционной системы. Список тем опе рационной системы можно расширять. Microsoft время от времени добавляет но вые темы (так, в издания Media Center и Tablet PC для Windows XP была добав лена тема «royale»), но способ создания новых тем третьими сторонами не опуб ликован3. Обличья Темы и обличья (skins) часто путают, хотя это разные понятия. Обычно темой называют набор данных, применяемых для настройки внешнего вида приложе ния, а обличье – это результат модификации темы конечным пользователем. Например, в Microsoft Office есть богатый набор тем для собственного пользова тельского интерфейса, но создание обличий не поддерживается. Напротив, Windows Media Player обеспечивает широкую поддержку обличий, в том числе позволяет пользователям создавать новые темы. 3 Существует несколько разработанных третьими сторонами решений для добавления новых тем Windows, самый известный из них «ThemeXP». В основе их работы лежит модификация ба% зовых компонентов Windows, они подвергают угрозе безопасность системы, поэтому я насто% ятельно рекомендую ими не пользоваться. Лично я хотел бы, чтобы Microsoft раскрыла модель тем и позволила третьим сторонам расширять набор существующих, но, пока этого не прои% зошло, лучше ограничиться встроенными темами. 382 Глава 8. Стили WPF хорошо поддерживает темы, предоставляя авторам элементов управле ния и приложений возможность удивительно гибко изменять их внешний вид. Но никаких встроенных средств для создания обличий приложения в WPF нет. Впрочем, реализовать такой механизм тривиально, если понимать, как работа ют стили и темы. Коль скоро тема – это не что иное, как объединение словаря ре сурсов с глобальным набором, то ничто не мешает реализовать обличья, написав небольшой по объему код для модификации набора ресурсов на уровне приложе ния, окна или контейнера. Для демонстрации создадим простое окно, в котором есть флажок (для перек лючения обличий) и две кнопки (выступающие в разных обличьях): <Window x:Class=’Styles.Skinning’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’Styles’ > <StackPanel> <CheckBox> Use custom buttons? </CheckBox> <Button Margin=’2’ HorizontalAlignment=’center’> A button </Button> <Button Margin=’2’ HorizontalAlignment=’center’> Another button </Button> </StackPanel> </Window> Эта программа ведет себя в точности так, как и ожидалось (рис. 8.2). Теперь можно определить две разных темы с помощью словаря ResourceDictionary. Первая будет совсем скучной, по сути ничего не делающей. Зато она позволит нам легко восстановить стандартный вид приложения: <!— DefaultButtons.xaml —> <ResourceDictionary xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ > <Style x:Key=’{x:Type Button}’ TargetType=’{x:Type Button}’> </Style> </ResourceDictionary> Вторая тема применяет созданный нами шаблон к любой кнопке в приложении: Введение в стили 383 <!— CustomButtons.xaml —> <ResourceDictionary xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ > <Style x:Key=’{x:Type Button}’ TargetType=’{x:Type Button}’> <Setter Property=’Template’> <Setter.Value> <ControlTemplate TargetType=’{x:Type Button}’> <Border CornerRadius=’10,2,10,2’ BorderThickness=’1’ BorderBrush=’Black’ Padding=’2’> <Border.Background> <LinearGradientBrush> <GradientStop Offset=’0’ Color=’#FFFFFFFF’ /> <GradientStop Offset=’1’ Color=’#FFCCCCCC’ /> </LinearGradientBrush> </Border.Background> <ContentPresenter HorizontalAlignment=’Center’ VerticalAlignment=’Center’ /> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary> Рис. 8.2. Демонстрационное приложение до изменения обличья Глава 8. Стили 384 Чтобы переключить стили, подпишемся на события CheckBox.Checked и CheckBox.Unchecked и обработаем их в одной общей функции. Еще необходимо присвоить флажку имя, чтобы можно было опросить его значение: <Window ... > ... <CheckBox x:Name=’_check1’ IsChecked=’False’ Checked=’ButtonChecked’ Unchecked=’ButtonChecked’> Use custom buttons? </CheckBox> ... </Window> Для загрузки подходящего ресурса мы можем воспользоваться методом Application.LoadComponent, который принимает URI и возвращает словарь ре сурсов: ResourceDictionary theme = Application.LoadComponent( new Uri(«DefaultButtons.xaml», UriKind.Relative) ) as ResourceDictionary; Пользуясь свойством MergedDictionaries объекта ResourceDictionary, мы мо жем добавить ресурсы из темы в набор ресурсов окна. Свойство MergedDictionaries позволяет оставить все локально определенные ресурсы без изменения, но добавить ресурсы темы. Для этого нужно клонировать словарь ре сурсов, связанный с окном: ResourceDictionary rd = new ResourceDictionary(); foreach (DictionaryEntry t in this.Resources) { rd[t.Key] = t.Value; } Собрав все вместе, мы реализуем объединение с новой темой и ассоциируем результат с окном4: void ButtonsChecked(object sender, RoutedEventArgs e) { ResourceDictionary rd = new ResourceDictionary(); foreach (DictionaryEntry t in this.Resources) 4 Оператор ?? в C# 2.0 применим к типу Nullable<T>. В данном случае, если _check1.IsChecked возвращает null, то значением выражения будет false. Введение в стили 385 { rd[t.Key] = t.Value; } if (_check1.IsChecked ?? false) { ResourceDictionary theme = Application.LoadComponent( new Uri(«DefaultButtons.xaml», UriKind.Relative) ) as ResourceDictionary; rd.MergedDictionaries.Add(theme); } else { ResourceDictionary theme = Application.LoadComponent( new Uri(«CustomButtons.xaml», UriKind.Relative) ) as ResourceDictionary; rd.MergedDictionaries.Add(theme); } this.Resources = rd; } Эта программа выводит окно, показанное на рис. 8.3. Рис. 8.3. Демонстрационное приложение после изменения обличья Эту логику легко можно вынести из реестра или конфигурационного файла. Кроме того, обращение к методу LoadComponent можно было бы заменить пря мым вызовом XamlReader, что позволило бы загружать XAMLопределение темы из любого места (метод LoadComponent умеет загружать только ресурсы, вклю ченные в манифест сборки). Глава 8. Стили 386 Наследование стилей Все элементы управления WPF основаны на стилях и шаблонах. При проек тировании мы считали, что сами элементы управления могут и не содержать ни каких «зашитых» значений свойств, умолчаний или визуальных элементов, пос кольку графический дизайнер и автор темы должны иметь полный контроль над поведением элемента по умолчанию. Примеры, приведенные в этой главе, пока зывают, что с помощью стиля можно настроить лишь немногие свойства элемен тов управления, так откуда же берутся остальные? Для каждого свойства в WPF определено понятие приоритета значений (рис. 8.4). Все, что мы видели до сих пор, – стили, привязка к данным, наследование и прочее – применяется в строго определенном порядке. Интересно отметить, что к любому элементу на самом деле применяется два стиля. При использовании элемента управления создается впечатление, что стиль только один, поскольку второй зарезервирован для автора элемента. В любом случае локальные значения свойств имеют приоритет по отношению к определенным в стиле. Локальное значение свойства 1 Стиль по умолчанию Стиль 2 3 Элемент управления Рис. 8.4. Приоритеты значений свойств Помимо элементарной расстановки приоритетов, имеется еще возможность создать базовый стиль, которому могут наследовать другие стили. У каждого сти ля может быть только один родитель (специально для любителей C++ – множе ственного наследования нет), зато один и тот же базовый стиль может служить родителем для нескольких стилейпотомков. С учетом иерархии стилей рис. 8.4 превращается в рис. 8.5. Пусть нужно создать базовый стиль, который будет применяться ко всем эле ментов управления и содержать некоторые общие свойства шрифта. Для этого определим стиль, указав в качестве целевого общий базовый тип, в котором уже есть все свойства, подлежащие настройке, и зададим для него имя: <Style x:Key=’baseControls’ TargetType=’{x:Type Control}’> <Setter Property=’FontSize’ Value=’14pt’ /> <Setter Property=’FontFamily’ Value=’Corbel, Arial’ /> <Setter Property=’Margin’ Value=’2’ /> </Style> Введение в стили 387 Теперь этот базовый тип можно ассоциировать с различными типами элемен тов управления, пользуясь стандартным тегом Style и его свойством BasedOn5: Основан на 3 Локальное значение свойства 1 Стиль по умолчанию Стиль 2 4 Элемент управления Рис. 8.5. Приоритеты значений свойств с учетом наследования стилей <Style x:Key=’{x:Type TextBox}’ TargetType=’{x:Type TextBox}’ BasedOn=’{StaticResource baseControls}’ /> Чтобы осознать все заключенные тут возможности, создадим парочку стилей для других типов элементов управления. Помимо ранее установленных свойств, скажем еще, что надпись на кнопке должна рисоваться полужирным шрифтом, как бы шрифт ни был определен в базовом стиле: <Window x:Class=’Styles.StyleInheritence’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’Styles’ > <Window.Resources> 5 Обратите внимание на использование ключевого слова StaticResource в приведенном ниже фрагменте. Поскольку сам объект Style не пользуется системой зависимых свойств, то для за% дания его свойств мы не можем прибегнуть к связыванию, стилизации или динамическим ре% сурсам. Слово StaticResource позволяет применить ресурс ровно один раз, а недостаток тако% го решения состоит в том, что свойство BasedOn стиля не обновляется при модификации сло% варя ResourceDictionary. Глава 8. Стили 388 <Style x:Key=’baseControls’ TargetType=’{x:Type Control}’> <Setter Property=’FontSize’ Value=’14pt’ /> <Setter Property=’FontFamily’ Value=’Corbel, Arial’ /> <Setter Property=’Margin’ Value=’2’ /> </Style> <Style x:Key=’{x:Type Button}’ TargetType=’{x:Type Button}’ BasedOn=’{StaticResource baseControls}’> <Setter Property=’FontWeight’ Value=’Bold’ /> </Style> <Style x:Key=’{x:Type CheckBox}’ TargetType=’{x:Type CheckBox}’ BasedOn=’{StaticResource baseControls}’ /> <Style x:Key=’{x:Type TextBox}’ TargetType=’{x:Type TextBox}’ BasedOn=’{StaticResource baseControls}’ /> </Window.Resources> <StackPanel> <CheckBox>Hello World</CheckBox> <TextBox>Hello World</TextBox> <Button>Hello World</Button> <Button>Hello World</Button> </StackPanel> </Window> Запустив эту программу, мы увидим, что все три типа элементов управления получают семейство шрифтов и их размер из стиля baseControls, а надписи на кнопках к тому же выводятся полужирным шрифтом (рис. 8.6). Итак, мы познакомились с основными возможностями стилей. Но, пожалуй, самое важное в стилях – это то, как ими использоваться, чтобы улучшить, а не ис портить приложение. Применение стилей не во зло Включение стилей, да и вообще настраиваемость всего и вся в WPF, наверное, можно считать одним из самых существенных изменений по сравнению со всеми предшествующими презентационными технологиями. При работе над WPF мы часто сравнивали то, что делали, с оригинальной системой Macintosh, которая предложила обычному пользователю богатый набор шрифтов и типографичес ких средств. Человечество в течение целых десяти лет расплачивалось за это до кументами, выглядящими как нотная партитура, пока не научилось пользовать ся шрифтами ответственно, чтобы улучшить способы передачи информации. Применение стилей не во зло 389 Самоограничение необходимо и при работе со стилями. Есть три основных причины для создания специализированного стиля: (1) предоставить общий на бор свойств нескольким объектам; (2) выполнить все настройки в одном месте; (3) сделать приложение не похожим на другие. Рис. 8.6. Использование базового стиля для распространения общих свойств на несколько дочерних стилей Применять стили ради решения первых двух задач вполне оправдано, и я го рячо рекомендую такой подход. Если придерживаться разумных рекмендаций (ведь следуем же мы рекомендациям при написании кода), то стили могут замет но повысить производительность труда. А переносить ли определения стилей в отдельный файл или нет – дело вкуса. Что же касается использования стилей для придания приложению уникаль ного внешнего вида, то я хочу сделать три предостережения: Не пытайтесь свести определение внешнего вида лишь к созданию одного сти ля. Это должно быть сквозной темой всего приложения. Включайте в тему достаточное число стандартных элементов управления, что бы приложение выглядело единообразным. В единообразии сила. Сформулируйте для себя главную идею. Создавайте темы, а не стили Специализированные темы имеют тенденцию очень быстро разрастаться. Определения нескольких градиентов и фигур для элементов управления могут 390 Глава 8. Стили занимать несколько сотен строк в разметке. Кроме того, мы начинаем вводить зависимости между шаблонами и стилями, а также между ресурсами и шабло нами (обратили внимание на круговую зависимость?). Зачастую для определе ния развитой темы требуется программировать конвертеры значений или нес тандартное поведение элемента. Следовательно, в общем случае лучше предс тавлять себе тему как нечто целостное, а не просто набор стилей при корневом окне приложения. Первый мой конкретный совет – вынести определение темы в отдельный файл и воспользоваться функцией объединения словарей для привязки темы к приложению. А еще лучше поместить определение темы в отдельную сборку и загружать стиль с помощью перекрестных ссылок на сборки (/<assemblyName>;component/ <resourceName>): <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source=’/StyleLibrary;Component/theme.xaml’ /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> Такой подход позволяет четко отделить тему от приложения, а это упрощает передачу ее дизайнеру и вынуждает нас думать о теме как об отдельном компо ненте. В единообразии сила Продумывая, как должно выглядеть приложение, важно помнить о единооб разии всех используемых элементов управления. Одна из задач WPF – обеспе чить настраиваемость всех элементов, чтобы не получались «приложенияфран кенштейны». Такое явление часто можно наблюдать при использовании прежних библиотек для создания форм (они выглядят так, будто сшиты из разных кус ков). Разумеется, такая могучая функциональность предъявляет повышенные требования к разработчику тем, зато создает благоприятное впечатление у поль зователя. Добиться единообразия будет проще, если заранее выбрать название темы («металлик», «плоская» и т.д.). Взгляните на «плохой» набор элементов управле ния, показанный на рис. 8.7 слева. Здесь гарнитуры и размеры шрифтов различ ны, цвета тоже различны, даже углы наклона текста отличаются. Верхняя кноп ка, наверное, неплохо выглядела бы в игре или в причудливом интерфейсе для анкетирования, а непритязательные на вид переключатели подошли бы Web приложению. Не претендуя на лавры графического дизайнера, я все же осмелюсь утверждать, что «хорошие» элементы справа по крайней мере единообразны. У них единый стиль, выбор шрифтов и т.д. Применение стилей не во зло Плохие 391 Хорошие Рис. 8.7. Единообразие темы исключительно важно для создания удобного для пользователя приложения Помимо единообразия, набор элементов управления должен быть полон. Обычно, приступая к созданию темы, человек выбирает несколько общеупотребительных прос тых элементов, например, различные кнопки. Но затем он начинает конструировать ре альное приложение, в котором встречаются самые разнообразные элементы, требую щие более сложных стилей, и в результате все заканчивается тем же «Франкештей ном», только немножко иного вида (рис. 8.8). Одна из самых распространенных оши бок такого рода заключается в том, что забывают стилизовать полосы прокрутки. На рис. 8.8 кнопки выглядят «круто», а ползунок и вкладки вообще никак не настроены. После создания стиля, отличающегося единообразным внешним видом и ши риной элементов управления, нам понадобится нечто, связывающее тему воедино. Рис. 8.8. Единообразие означает, что тема охватывает все элементы управления или, по крайней мере, все используемые в приложении Сформулируйте главную идею Темы позволяют придать приложению уникальный внешний вид. Создание специализированной темы – трудоемкое занятие как в плане дизайна, так и в пла не реализации. Главное – чтобы тема вызывало у пользователя определенные ас социации. Быть может, идеей темы является продвижение бренда компании, или «экстремальная» стилизация, заявляющая о необычности приложения. Важно, чтобы идею темы можно было выразить словами, лучше всего одним предложе нием. Например, команда WPF частенько возвращается к примеру Xbox, мечтая создать соответствующую тему. У приставки Xbox очень характерный облик в плане аппаратуры, программного обеспечения и даже присутствия в Web. Тема по образцу Xbox несла бы в себе очевидное послание; она отличала бы приложе ния для Xbox от прочих приложений для Windows. 392 Глава 8. Стили Вообще говоря, если программа представляет собой обычную утилиту или стандартное деловое приложение, то маловероятно, что специальная тема доба вит ему ценности. Но даже в таких случаях было бы интересно воспользоваться темами, чтобы привести приложение в соответствие с рекомендациями по разра ботке удобных для пользователей программ. Чего мы достигли? В этой главе мы рассмотрели работу стилей и тем в WPF. Мы видели, как оп ределяются и используются стили и как они собираются в темы. Мы познакоми лись с рекомендациями по применению стилей. И напоследок мы узнали, как с помощью ресурсов и шаблонов стили сопрягаются с системой работы с данными в WPF. Стили объединяют между собой все части WPF. 393 Приложение. Базовые службы Синкатегориматический – не могущий быть использованным в качестве термина сам по себе; так говорят о таких членах предложения, как наре чие или предлог. Полный исправленный словарь Вэбстера, 1996, 1998 MICRA, Inc. На платформе Windows Presentation Foundation имеется весьма развитый на бор служб. В настоящей книге я ставил себе целью изложить основные концеп ции платформы, что и было сделано в предшествующих восьми главах. Но служ бы зачастую оказываются очень полезны, и их рассмотрение поможет понять, как построена система в целом и как ее части сопрягаются друг с другом. В этом при ложении я расскажу о некоторых из своих любимых служб и продемонстрирую, как ими можно воспользоваться в приложении. Потоки и диспетчеры В самом начале разработки WPF мы задались целью устранить зависимость от модели с однопотоковым контейнером (STA). Для многих элементов управле ния ActiveX и других служб на основе COM необходима именно потоковая мо дель STA, требующая, чтобы в каждый момент времени код компонента испол нялся только в одном потоке, причем всегда одном и том же. В настоящее время почти все пользовательские интерфейсы в Windows работают в предположении модели STA. Проблема STA заключается в том, что все объекты привязаны к единственно му потоку и не могут переместиться ни в какой другой. Требование о том, чтобы некий фрагмент кода исполнялся одним потоком, – это очень эффективное упро щение, но раз и навсегда привязывать исполнение к единственному потоку, – по жалуй, чересчур. Увы, когда версия «Longhorn» превратилась в Vista, мы осозна ли, что для того, чтобы WPF могла работать с существующими службами (буфер обмена, Internet Explorer и т.д.), нам придется поддержать STA. Поэтому мы ре шили создать одну потоковую модель, а не поддерживать несколько. Поэтому, к моему величайшему сожалению, я вынужден сообщить, что WPF требует потоковой модели STA. Впервые мы сталкиваемся с этим требованием при написании простейшего приложения «Здравствуй, мир»; его точка входа по мечена атрибутом STAThread: using System; using System.Windows; 394 Приложение. Базовые службы class Program { [STAThread] static void Main() { Application app = new Application(); Window w = new Window(); w.Show(); app.Run(); } } Большинство объектов WPF наследуют общему базовому типу DispatcherObject. Отчасти вследствие первоначального намерения избавиться от STA мы привязываем объекты не к потоку, а к одному диспетчеру. Диспетчер – это объект, который получает сообщения (в CLR они называются событиями, в библиотеке User32 – сообщениями) и направляет их нужному объекту. Типы, имеющие отношение к диспетчеру и к потоковой модели WPF, по большей час ти находятся в пространстве имен System.Windows.Threading. Истинный параллелизм с разделением памяти (многопоточность) почти не возможно запрограммировать в основных современных языках. Проблема в том, что приходится иметь дело со сложными правилами управления блокировками и параллелизмом. В случае некорректной работы с блокировками мы очень быстро столкнемся с «гонкой», клинчами, активными взаимоблокировками и порчей па мяти. Самая успешная модель одновременного выполнения нескольких задач – это организация слабой связи между двумя задачами и применение асинхронных сообщений для передачи данных между двумя потоками. Среди изменений в поддержке многопоточности, появившихся в .NET Framework 2.0, стоит упомянуть класс SynchronizationContext. Он позволяет со общить системе, как управлять одновременным выполнением. Конкретно, в слу чае WPF в контексте сохраняется ссылка на диспетчера, в результате чего любой асинхронный обратный вызов от различных компонентов (например, System.Net.WebClient) становится возможно переадресовать потоку пользова тельского интерфейса. Это происходит автоматически, так что мы можем вообще не заботиться о проблемах, связанных с многопоточностью. Чтобы посмотреть, как это происходит на практике, напишем программу, ко торая загружает HTMLстраницу с сайта. Сделать это проще всего, воспользо вавшись методом DownloadStringAsync из класса WebClient: <Window x:Class=’ThreadingExample.Window1’ xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’ xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’ Title=’ThreadingExample’ > <StackPanel> <TextBox x:Name=’_text’ Height=’150’ HorizontalScrollBarVisibility=’Auto’ VerticalScrollBarVisibility=’Auto’ /> <Button Click=’DownloadNormally’>Download</Button> Потоки и диспетчеры 395 </StackPanel> </Window> using System; ... namespace ThreadingExample { public partial class Window1 : Window { public Window1() { InitializeComponent(); } void DownloadNormally(object sender, RoutedEventArgs e) { WebClient wc = new WebClient(); wc.DownloadStringCompleted += delegate(object sender2, DownloadStringCompletedEventArgs e2) { _text.Text = e2.Result; }; wc.DownloadStringAsync(new Uri(«http://www.msn.com»)); } } } Рис. A.1. Асинхронная загрузка HTML%страницы Эта программа выводит результат, показанный на рис. A.1. Объект типа WebClient получает объект SynchronizationContext и затем использует его для воз буждения событий (это действие приводит к тому, что в потоке пользовательского интерфейса вызываются делегаты). Такая модель позволяет программировать асинхронные действия, не вдаваясь в тонкости многопоточной обработки. Приложение. Базовые службы 396 Иногда бывает необходимо организовывать многопоточность явно, нап ример, чтобы создать фоновый поток, в котором выполняется задача, занима ющая много времени. Для таких случаев предусмотрено два способа обмена данными с потоком пользовательского интерфейса. Если имеется повторно используемый компонент, который может работать и вне WPFприложения (например, в приложении Windows Forms или ASP.NET), то нужно самосто ятельно работать с обратными вызовами с помощью SynchronizationContext, как это делает WebClient. Если же реализуется прикладная логика, специ фичная только для конкретной программы, то можно напрямую обращаться к диспетчеру, ассоциированному с потоком пользовательского интерфейса. Это дает более полный контроль над способом обработки сообщений, но мы уже не направляем обратные вызовы в правильный контекст в Windows Forms или ASP.NET. Независимо от метода создания компонента составные части остаются те ми же самыми. Нам необходим объект, представляющий задачу, и объект, представляющий результирующее «сообщение». В примере ниже создается задача, которая вычисляет сумму целых чисел от 0 до n1. Результатом явля ется объект класса, производного от EventArgs, который содержит получив шееся значение: class SumCompletedEventArgs : EventArgs { int _result; public int Result { get { return _result;} set { _result = value;} } } Для объектазадачи достаточно простейшей объектной модели, которая долж на быть согласована с паттерном асинхронного компонента. Нам понадобится метод <DoSomething>Async и событие <DoSomething>Completed: class Sum { public Sum() { ... } public event EventHandler<SumCompletedEventArgs> SumCompleted; public void SumAsync(int value) { ... } } В данном случае обратными вызовами будет заниматься объект SynchronizationContext, поэтому в конструкторе мы запомним текущий кон текст: class Sum { SynchronizationContext _context; Потоки и диспетчеры 397 public Sum() { _context = SynchronizationContext.Current; } ... } Чтобы эта задача выполнялась в фоновом потоке, требуется еще написать две функции. Первая реализует то, что собственно выполняется в фоновом потоке; ее мы назовем BackgroundTask. Вторая – Completed – это обратный вызов, который будет исполняться в потоке пользовательского интерфейса. Обратите внимание, что из фонового потока мы не обращаемся к состоянию объекта, поэтому всякие конфликты исключены. Чтобы избежать тонких ошибок, связанных с многопо точностью, доступ к данным осуществляется только из потока пользовательско го интерфейса. Все необходимые данные нужно скопировать в аргументы, пере даваемые фоновому потоку: class Sum { SynchronizationContext _context; public Sum() { _context = SynchronizationContext.Current; } public event EventHandler<SumCompletedEventArgs> SumCompleted; public void SumAsync(int value) { Thread background = new Thread(BackgroundTask); background.IsBackground = true; background.Start(value); } void BackgroundTask(object parameter) { int value = (int)parameter; int result = 0; for (int i = 0; i <= value; i++) { result += i; Thread.Sleep(10); } _context.Post(Completed, result); } void Completed(object result) { if (SumCompleted != null) { SumCompletedEventArgs e = new SumCompletedEventArgs(); e.Result = (int)result; SumCompleted(this, e); } } } 398 Приложение. Базовые службы Этот класс работает точно так же, как WebClient. Мы создаем объект, подпи сываемся на событие и вызываем метод SumAsync: void RunTask(object sender, RoutedEventArgs e) { Sum t = new Sum(); t.SumCompleted += delegate(object sender2, SumCompletedEventArgs e2) { text.Text = e2.Result.ToString(); }; t.SumAsync(250); } Главное достоинство использования диспетчера WPF вместо объекта SynchronizationContext – возможность задать приоритет обратного вызова пото ка пользовательского интерфейса. В перечислении System.Windows.Threading. DispatcherPriority определено 12 таких приоритетов. Впрочем, проще воспользо ваться имеющимся в .NET компонентом BackgroundWorker, который сам работа ет с SynchronizationContext. Свойства При проектировании типов мы говорим об их свойствах, методах и событиях. Именно этими тремя понятиями объект описывается с точки зрения разработчи ка. В предыдущей компонентной модели, созданной корпорацией Microsoft, – COM – поддерживались только методы. Поддержка свойств была рудиментар ной – в определении интерфейса были метаданные, помечающие методы put_ и get_, чтобы инструменты типа VB могли предоставить модель, включающую свойства. События же были реализованы напрямую с помощью паллиативных стоков (sinks) для обратных вызовов. При проектировании .NET была, в частности, поставлена цель поддержать все три концепции на уровне самой платформы. События и свойства были реализо ваны как в среде исполнения, так и почти во всех .NETсовместимых языках. WPF от начала до конца реализована в виде управляемого кода. Это решение бы ло принято на ранних стадиях разработки WPF, чтобы не отклоняться от согла шений и паттернов, принятых в .NET, и иметь возможность обращаться к сред ствам среды исполнения. Тогда какое же место занимает система свойств WPF? Свойства .NET Мы сейчас рассматриваем службы самого низкого уровня, но интересно по размыслить о том, что мы собираемся построить на их основе. Мы хотим полу чить динамическую, управляемую данными, декларативную, композиционную систему презентации. Начнем с самого начала – с CLRсвойства некоего фиктивного типа: public class Widget { Widget _parent; Color _background; Свойства 399 public Color Background { get { return _background; } set { _background = value; } } public Widget Parent { get { return _parent; } set { _parent = value; } } } Мы определили два свойства: Background и Parent. И сразу же сталкиваемся с проблемой – мы хотим, чтобы при изменении свойства Background объект Widget перерисовывал себя. Разумеется, у других разработчиков может возник нуть желание узнать о том, что цвет фона изменился, поэтому извещение об из менении следует сделать открытым: public class Widget { Widget _parent; Color _background; public event EventHandler BackgroundChanged; public Color Background { get { return _background; } set { if (_background != value) { _background = value; OnBackgroundChanged(EventArgs.Empty); } } } public Widget Parent { get { ... } set { ... } } protected virtual void OnBackgroundChanged(EventArgs e) { if (BackgroundChanged != null) { BackgroundChanged(this, e); } RepaintWidget(); } } Пока все просто. Далее, очень полезно было бы иметь возможность устанавли вать такой же цвет фона, как у родителя: public class Widget { Widget _parent; bool _isBackgroundSet = false; Color _background; Приложение. Базовые службы 400 public event EventHandler BackgroundChanged; public Color Background { get { if (!isBackgroundSet && Parent != null) { return Parent.Background; } return _background; } set { if (_background != value) { _background = value; _isBackgroundSet = true; OnBackgroundChanged(EventArgs.Empty); } } } public Widget Parent { get { ... } set { ... } } protected virtual void OnBackgroundChanged(EventArgs e) { ... } } Но и это еще не конец. Мы забыли о двух важнейших вещах. Вопервых, если родитель изменился, а цвет фона не установлен, нужно отправить извещение об изменении. Вовторых, необходим механизм для восстановления цвета фона по умолчанию (унаследованного): public class Widget { Widget _parent; bool _isBackgroundSet = false; Color _background; public event EventHandler BackgroundChanged; public event EventHandler ParentChanged; public Color Background { get { ... } set { ... } } public Widget Parent { get { ... } set { if (_parent != value) { _parent = value; OnParentChanged(EventArgs.Empty); } } } public void ResetBackground() { _isBackgroundSet = false; Свойства 401 OnBackgroundChanged(EventArgs.Empty); } protected virtual void OnBackgroundChanged(EventArgs e) { ... } protected virtual void OnParentChanged(EventArgs e) { if (ParentChanged != null) { ParentChanged(this, e); } if (!_isBackgroundSet) { OnBackgroundChanged(EventArgs.Empty); } } } Уже видно, что много кода дублируется. Напомним, что ключевой частью ди намической, управляемой данными, декларативной системы является идея сти лизации. Чтобы повысить степень повторной используемости кода, в WPF име ется возможность определить единственный объект так, что он сможет наделять своими свойствами другие объекты; это похоже на использование стилей в Microsoft Word или CSSстилей в HTML. Конечно, мы хотим, чтобы наш класс Widget поддерживал эту функциональность: public class Widget { Style _style; Widget _parent; bool _isBackgroundSet = false; Color _background; public event EventHandler BackgroundChanged; public event EventHandler ParentChanged; public event EventHandler StyleChanged; public Color Background { get { if (!isBackgroundSet) { if (Style != null) { return Style.Background; } return Parent.Background; } return _background; } set { ... } } public Widget Parent { get { ... } set { ... } } public Style Style { get { ... } set { ... } } public void ResetBackground() { ... } Приложение. Базовые службы 402 protected virtual void OnBackgroundChanged(EventArgs e) { ... } protected virtual void OnParentChanged(EventArgs e) { ... } protected virtual void OnStyleChanged(EventArgs e) { ... } } В этом примере мы добавили стандартный код для поддержки свойства Style и коечто интересное внутри метода get для свойства Background. Чтобы соотве тствовать требованиям WPF, предстоит еще добавить немало функциональности в свойства (анимацию, значения по умолчанию и т.д.). Но прежде чем заходить так далеко, посмотрим, что же у нас получается. Для начала отметим, что любое унаследованное свойство должно быть опре делено в базовом типе. Поскольку мы зашили в код поиск свойства в родительс ком типе, то выходит, что на этапе компиляции базового типа мы уже должны знать обо всех свойствах, которые ктото может захотеть унаследовать. То же са мое справедливо и для стилей. Так как определение Style приходится обновлять для каждого свойства, то стилизации поддаются только те свойства, о которых мы знаем заранее. Мы дублируем значительный объем код для извещения об из менениях. Мы вынуждены зашивать в код зависимости между свойствами; конк ретно, нам пришлось в родительском классе написать код для извещения об из менении цвета фона. Оказалось, что необходимо заводить в классе локальное по ле для каждого свойства. Список можно продолжить. Система свойств WPF Проанализировав все это, команда разработчиков WPF выделила три службы, которые должна поддерживать система свойств: Уведомления об изменении. Отслеживание зависимостей. Выражения. На первый взгляд, кажется, что этого маловато, но мы рассуждали следующим образом: если мы можем узнать, что некое свойство изменилось, и понимаем, ка кие свойства затрагиваются этим изменением, то сумеем реализовать сложные выражения, которые поддерживают многие другие механизмы (наследование, стили и т.д.). С помощью этих трех краеугольных камней мы сможем смоделиро вать и другие службы в системе свойств. Единственное, что нигде не документи ровано, – это выражения. В первой версии WPF выражения предназначены только для внутреннего пользования. Вместо них WPF раскрывает богатую встроенную функциональ ность: Разреженное хранилище. Стилизация. Анимация. Связывание. Присоединенные свойства. Свойства 403 Существует базовый класс, который инкапсулирует все это поведение доста точно общим способом: public class DependencyObject : DispatcherObject { public DependencyObject(); public void ClearValue(DependencyProperty dp); public object GetValue(DependencyProperty dp); protected virtual void OnPropertyChanged (PropertyChangedEventArgs e); public void SetValue(DependencyProperty dp, object value); // многие методы опущены, чтобы не смущать читателя } Здесь показаны три основные функции (получить, установить и очистить) и обратный вызов (уведомление об изменении). Объект DependencyProperty од нозначно идентифицирует свойство; его можно считать программным именем свойства. Класс DependencyObject предоставляет разреженное хранилище. Это сущест венно: для свойств, определенных с помощью системы свойств WPF, нет никаких накладных расходов на каждый экземпляр, если только для них не задано значе ние, отличное от подразумеваемого по умолчанию. Разреженному хранилищу было уделено много внимания при проектировании системы свойств, поскольку мы понимали, что каркас будет интенсивно обращаться к свойствам (только для управления рендерингом текста имеется более 40 свойств!), так что любые нак ладные расходы на уровне экземпляра приведут к падению производительности. Исходный текст класса DependencyObject выглядит примерно так: public class DependencyObject : DispatcherObject { IDictionary<DependencyProperty,object> _values = new Dictionary<DependencyProperty, object>(); public DependencyObject(); public void ClearValue(DependencyProperty dp) { _values.RemoveValue(dp); } public object GetValue(DependencyProperty dp) { if (_values.ContainsKey(dp)) return _values[dp]; return dp.DefaultValue; } public void SetValue(DependencyProperty dp, object value) { _values[dp] = value; } } На самом деле, логика сложнее, но и из этой упрощенной версии видно, что накладные расходы сводятся к одному объекту в словаре, поэтому мы можем за водить сколько угодно свойств. Но такая гибкость не обходится даром; если боль шинство свойств устанавливаются локально (а не с помощью стилей, наследова ния или значения по умолчанию), то производительность окажется хуже, чем при использовании локальных значений. Приложение. Базовые службы 404 Преобразовать код класса Widget, так чтобы он пользовался системой свойств, тривиально: public class Widget : DependencyObject { public static readonly DependencyProperty StyleProperty = DependencyProperty.Register(«Style», typeof(Style), typeof(Widget)); public static readonly DependencyProperty BackgroundProperty = DependencyProperty.Register(«Background», typeof(Color), typeof(Widget)); public static readonly DependencyProperty ParentProperty = DependencyProperty.Register(«Parent», typeof(Widget), typeof(Widget)); } Но было бы невежливо остановиться на этом (к тому же большинство инстру ментов перестало бы работать!). Напомним, что WPF обещает следовать согла шениям, принятым в .NET, а одним из таких соглашений является встроенная в .NET система свойств. Система зависимых свойств спроектирована с целью до полнить, а не заменить то, что уже есть в .NET: public class Widget : DependencyObject { public static readonly DependencyProperty StyleProperty = ...; public static readonly DependencyProperty BackgroundProperty = ...; public static readonly DependencyProperty ParentProperty = ...; public get set } public get set } public get set } Style Style { { return (Style)GetValue(StyleProperty); } { SetValue(StyleProperty, value); } Color Background { { return (Color)GetValue(BackgroundProperty); } { SetValue(BackgroundProperty, value); } Widget Parent { { return (Widget)GetValue(ParentProperty); } { SetValue(ParentProperty, value); } } Метаданные В .NET уже есть понятие метаданных – мы можем пометить атрибутом любой член, тип или сборку. Но у системы имеются ограничения, и одно из них – про изводительность. Для системы зависимых свойств высокая производительность операций и выявление некоторых аспектов свойства критически важны, и в част ности по этой причине мы решили, что при объявлении зависимого свойства не обходимо сообщать его тип (чтобы не прибегать к отражению). Свойства 405 В системе зависимых свойств метаданные хранятся двумя способами. Вопер вых, часть информации хранится в самом свойстве, а именно: имя и тип свойства, а также тип, в котором это свойство объявлено (владелец). Имена свойств в пре делах каждого типавладельца должны быть уникальны. Кроме того, метаданные хранятся в объекте PropertyMetadata. Хотя информация в классе DependencyProperty фиксирована, мы вольны определять новые свойства для типа производного от PropertyMetadata. Класс PropertyMetadata позволяет контроли ровать дополнительные метаданные, описывающие поведение свойства (DefaultValue и IsReadOnly), глобально подписываться на извещения об изменениях (PropertyChangedCallback) и выполнять приведение значений (CoerceValueCallback). DefaultValue определяет, какое значение вернуть, если свойство не было уста новлено явно. Обычно возвращается значение по умолчанию для соответствую щего типа данных (null для всех ссылочных типов, 0 для числовых, false для буле вского и т.д.). IsReadOnly говорит, что свойство предназначено только для чтения. В .NET запрещение изменять поля проверяется на нижнем уровне верификатором промежуточного языка, который гарантирует, что полю, предназначенному толь ко для чтения, значение присваивается ровно один раз внутри конструктора. В системе зависимых свойств гарантия слабее; требуется лишь, чтобы значение свойству мог присваивать лишь типвладелец (или экземпляр этого типа). Что такое глобальное извещение об изменении свойства, понять не так уж слож но, а вот о приведении значений стоит поговорить подробнее. Проще всего разоб раться в этом на примере элемента управления Slider. Этот элемент поддерживает три свойства: Minimum, Maximum и Value. Требуется, чтобы Value всегда лежало между Minimum и Maximum, но ведь задавать значения свойств можно в любом по рядке. Чтобы не нарушать граничных условий и в то же время не жертвовать гиб костью, мы должны разрешить запись в свойство Value любого значения, но при не обходимости привести его в пределы диапазона между Minimum и Maximum. Первым шагом реализации приведения значения является определение вы шеупомянутых свойств: public class Slider : DependencyObject { public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register(«Minimum», typeof(int), typeof(Slider), new PropertyMetadata(0)); public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(«Maximum», typeof(int), typeof(Slider), new PropertyMetadata(100)); public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(«Value», typeof(int), typeof(Slider), new PropertyMetadata(50)); Приложение. Базовые службы 406 public int Minimum { get { return (int)GetValue(MinimumProperty); } set { SetValue(MinimumProperty, value); } } public int Maximum { get { return (int)GetValue(MaximumProperty); } set { SetValue(MaximumProperty, value); } } public int Value { get { return (int)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } } } Поскольку мы хотим, чтобы значение Value всегда лежало между Minimum и Maximum, введем новый статический метод для выполнения приведения и вклю чим его в состав метаданных для Value: public class Slider : DependencyObject { public static readonly DependencyProperty MinimumProperty = ...; public static readonly DependencyProperty MaximumProperty = ...; public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(«Value», typeof(int), typeof(Slider), new PropertyMetadata(50, null, ConstrainValue)); static object ConstrainValue(DependencyObject target, object baseValue) { int min = (int)target.GetValue(MinimumProperty); int max = (int)target.GetValue(MaximumProperty); int value = (int)baseValue; return Math.Min(Math.Max(value, min), max); } public int Minimum { get { ... } set { ... } } public int Maximum { get { ... } set { ... } } public int Value { get { ... } set { ... } } } Здесьто и кроется хитрость. Одно из основных достоинств системы свойств является то, что она пытается распознать все зависимости между свойствами, чтобы эффективно кэшировать значения и уведомлять об изменениях. Приведе ние вводит новую зависимость. Поскольку не существует общего механизма рас Клавиатура, мышь и стилос 407 ширения для добавления зависимостей, нам приходится вручную указывать сис теме свойств, как извещать об изменениях. Мы можем добавить глобальный об работчик изменения свойств Maximum и Minimum и сделать значение Value не действительным, когда то или другое изменяется: public class Slider : DependencyObject { public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register(«Minimum», typeof(int), typeof(Slider), new PropertyMetadata(0, ValueDependencyChanged)); public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(«Maximum», typeof(int), typeof(Slider), new PropertyMetadata(100, ValueDependencyChanged)); public static readonly DependencyProperty ValueProperty = ...; static void ValueDependencyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) { target.InvalidateProperty(ValueProperty); } static object ConstrainValue(DependencyObject target, object baseValue) { ... } public int Minimum { get { ... } set { ... } } public int Maximum { get { ... } set { ... } } public int Value { get { ... } set { ... } } } Помимо базового типа PropertyMetadata, существует еще важный производ ный тип FrameworkPropertyMetadata, который позволяет настроить поведение свойства по отношению к высокоуровневым средствам WPF, например: связыва нию, стилям и наследованию. Клавиатура, мышь и стилос Во времена доброго старого User32 для обработки специальных клавиш (например, комбинации Ctrl+S, означающей сохранение) надо было либо при менять таблицу акселераторов1, либо самостоятельно обрабатывать сообщения WM_KEYDOWN и WM_KEYUP. В Visual Basic было принято обрабатывать события KeyDown/KeyUp, поскольку не существовало никакого способа пост роить собственную таблицу акселераторов. У подхода, требующего сделать 1 С помощью таблиц акселераторов в User32 производилась трансляция нажатий клавиш в со% общения WM_COMMAND. Приложение. Базовые службы 408 нечто непосредственно в ответ на ввод с клавиатуры, есть два крупных недос татка. Вопервых, системы, в которых имеется редактор метода ввода (для японского, корейского, китайского и других языков), обычно в таком режиме не работают. Вовторых, пользователь теряет возможность назначить клави шам другие функции. Есть случаи, когда нужно общаться с устройством напрямую, например, реа гировать на движения стилоса в программе рисования, но обычно достаточно привязки к командам. Механизм привязки событий ввода к командам по умол чанию и так достаточно мощный, но он к тому же расширяем, то есть позволяет реализовать собственные правила выполнения команды. Структурировав приложение так, чтобы концепция привязки ввода была вы делена, мы сможем предоставить пользователю возможность настраивать при вязки по своему усмотрению. В главе 7 мы затрагивали вопрос о привязке ввода, а сейчас рассмотрим его подробнее. Класс InputBinding В классе InputBinding есть два интересных свойства: Gesture и Command. Свойство Gesture позволяет отобразить любой жест при вводе на заданную ко манду. И класс InputGesture (тип данных свойства Gesture), и интерфейс ICommand (тип данных свойства Command) расширяемы, что дает возможность отобразить любой жест на любую команду. По умолчанию WPF поддерживает жесты двух видов: от клавиатуры и от мыши, в результате чего мы можем постро ить стандартный набор акселераторов и поведения, необходимые для создания типичных приложений. В инструментах разработки встречается и более сложный вид жеста: клавиа турные аккорды. Аккорд состоит из нескольких клавиш, нажимаемых последо вательно (например, в редакторе Emacs аккорд Ctrl+X, S означает «сохранить»). Чтобы продемонстрировать расширяемость системы привязки, добавим в WPF привязку к клавиатурным аккордам. Жест, который мы собираемся поддержать, – это на самом деле набор клавиа турных жесов. Имеющийся в WPF тип KeyGesture не поддерживает клавиш без одновременно нажатых модификаторов (Ctrl, Shift и т.д.), но в аккордах вторая клавиша зачастую нажимается сама по себе. Первым делом определим новый класс KeyChordItemGesture, который соответствует одной «ноте» в аккорде. В классе InputGesture есть важный метод – Match, – который говорит, соответству ет ли данный жест какомунибудь событию ввода: public class KeyChordItemGesture : InputGesture { Key _key; ModifierKeys _modifiers; public Key Key { get { return _key; } set { _key = value; } } Клавиатура, мышь и стилос 409 public ModifierKeys Modifiers { get { return _modifiers; } set { _modifiers = value; } } public override bool Matches( object targetElement, InputEventArgs inputEventArgs) { KeyEventArgs e = inputEventArgs as KeyEventArgs; if (e != null) { return e.Key == Key && Keyboard.Modifiers == Modifiers; } return false; } } Теперь можно определить тип KeyChordGesture, содержащий набор жестов, которые должны сравниваться в порядке следования. В классе имеется очередь жестов; если произошло удачное сравнение, то жест из этой очереди удаляется. Если пользователь нажал не ту клавишу, очередь опустошается: [ContentProperty(«Gestures»)] public class KeyChordGesture : InputGesture { InputGestureCollection _gestures = new InputGestureCollection(); Queue<InputGesture> _chords = new Queue<InputGesture>(); public KeyChordGesture() { } public InputGestureCollection Gestures { get { return _gestures; } } public override bool Matches( object targetElement, InputEventArgs inputEventArgs) { if (_chords.Count == 0) { foreach (InputGesture k in _gestures) { _chords.Enqueue(k); } } if (_chords.Peek().Matches(targetElement, inputEventArgs)) { _chords.Dequeue(); return _chords.Count == 0; } else if (inputEventArgs is KeyEventArgs) { KeyEventArgs unmatched = (KeyEventArgs)inputEventArgs; if (unmatched.Key != Key.None && unmatched.Key != Key.LeftCtrl && unmatched.Key != Key.RightCtrl && unmatched.Key != Key.LeftShift && unmatched.Key != Key.RightShift && unmatched.Key != Key.LeftAlt Приложение. Базовые службы 410 && unmatched.Key != Key.RightAlt) { _chords.Clear(); } } return false; } } И последний шаг – определение привязки к вводу. У нас нет собственной объ ектной модели, которая включалась бы в привязку, но в базовом классе InputBinding отсутствует открытый конструктор, поэтому нужно лишь опреде лить тип так, чтобы его можно использовать в разметке: public class KeyChordBinding : InputBinding { public KeyChordBinding() { } } Теперь эту привязку можно использовать для любого элемента управления в наборе InputBindings: <Window.InputBindings> <l:KeyChordBinding xmlns:l=’clrnamespace:InputExample’ Command=’...some command...’> <l:KeyChordBinding.Gesture> <l:KeyChordGesture> <l:KeyChordItemGesture Key=’X’ Modifiers=’Control’ /> <l:KeyChordItemGesture Key=’S’ Modifiers=’Control’ /> </l:KeyChordGesture> </l:KeyChordBinding.Gesture> </l:KeyChordBinding> </Window.InputBindings> С помощью определения собственных типов привязки мы избегаем «зашива ния» в код зависимостей между логикой (обработчиками команд) и вводом от поль зователя. Это очень ценно, если приложение должно быть конфигурируемым, и к тому же дает гарантию, что мы сможем работать с любым способом ввода данных. Философия WPF состоит в том, чтобы перейти к более декларативному прог раммированию устройств ввода, поэтому такие механизмы, как привязка к вводу, находятся гораздо ближе к поверхности, тогда как прямое взаимодействие с уст ройствами отодвинуто подальше. Если все же возникает необходимость в работе с устройствами напрямую, то в WPF имеется развитая объектная модель. Взаимодействие с устройствами ввода Объектные модели всех устройств ввода имеют одну и ту же структуру. Суще ствует один статический служебный класс (Keyboard, Mouse, Stylus или Tablet) и класс устройства (KeyboardDevice, MouseDevice и т.д.). Служебный класс пре Клавиатура, мышь и стилос 411 доставляет глобальные средства (например, подключение непосредственно к со бытиям ввода и поиск устройства), а классы устройств – функциональность, спе цифичную для конкретного устройства. Наиболее употребительные свойства ос новного экземпляра данного устройства обычно размещаются в статическом слу жебном классе. Чтобы лучше освоиться с концепцией взаимодействия, рассмотрим клавиату ру. Выше в реализации класса KeyChordItemGesture вы могли заметить, что те кущее состояние клавишмодификаторов мы получаем от класса Keyboard: public override bool Matches( object targetElement, InputEventArgs inputEventArgs) { KeyEventArgs e = inputEventArgs as KeyEventArgs; if (e != null) { return e.Key == Key && Keyboard.Modifiers == Modifiers; } return false; } Keyboard.Modifiers – это статическое свойство, реализованное путем обраще ния к свойству Keyboard.PrimaryDevice.Modifiers. В системе может быть только одна клавиатура и только одна мышь. Однако разрешается подключать несколь ко планшетов (дигитайзеров), и с каждым планшетом может быть ассоциирован один или несколько стилосов. Поэтому свойство Tablet.TabletDevices возвраща ет набор всех планшетов, опознанных системой. При вводе с помощью мыши или стилоса целевой элемент управления опре деляется положением устройстваманипулятора. Свойство Capture позволяет закрепить устройство на указанном элементе на заданный период времени, но обычно фокус просто перемещается вместе с манипулятором. А вот при вводе с клавиатуры ситуация с фокусом выглядит совершенно иначе. Фокус клавиатуры В каждый момент времени только один элемент управления может владеть фокусом клавиатуры. К работе с фокусом имеют отношение два интересных класса: KeyboardNavigation и FocusManager. KeyboardNavigation отвечает за пе ремещение фокуса с одного элемента на другой при нажатии различных клавиш, а FocusManager – за отслеживание фокуса клавиатуры. Вообщето, про класс FocusManager можно и забыть, так как все представляющие интерес события и свойства этого класса встречаются в более удобных местах (например, в классах Keyboard и UIElement). Класс же KeyboardNavigation позволяет элементам управления решить, как они хотят реагировать на команды, изменяющие фокус клавиатуры. Чаще всего, наверное, применяются свойства IsTabStop и TabIndex. Обычно пользователь пе ремещает фокус клавиатуры с помощью клавиши Tab (если не считать мыши, ко нечно). Эта клавиша передает фокус логически следующему элементу, который может воспринимать данные от клавиатуры. 412 Приложение. Базовые службы Чего мы достигли? В этом приложении мы рассмотрели некоторые базовые службы, предостав ляемые платформой WPF. В типичном приложении вы не будете обращаться к этим службам напрямую, но знать об их существовании полезно, так как это по могает лучше понять, что можно построить на их основе. 413 Предметный указатель * *, оператор XPath, 317 . .NET Framework 3.0, расширения для Visual Studio, 59 модель данных, 295 онлайновые ресурсы, 59 простая модель программирования, 124 свойства, 396 / /, оператор XPath, 316 @ @, оператор XPath, 317 [ [], оператор XPath, 317 A Absolute, режим отображения, 226 Activated, событие Window, 86 AddHandler, метод, 352 AmbientLight, свойство, 258 AnimationClock, класс, 277 Application, объект, 69 вызов метода Run, 69 конструирование программы с по мощью разметки, 40 конфигурация, 76 нахождение всех открытых окон, 92 обработка ошибок, 73 определение, 70 открытое соглашение о пакетах, 83 создание при запуске приложения, 38 состояние, связанное с содержимым, 79 состояниедокумент, 85 управление временем жизни процес са, 72 управление состоянием, 75 Application.Current, свойство, 70 Application.StartupUri, свойство, 115 Application.Windows, свойство, 93 ApplicationSettingsBase, класс, 77 ArcSegment, 220 ArrangeOverride, метод, 187 ASP.NET, 32 assemblyIdentity, тег, 117 AutoReverse, свойство, класс Storyboard, 284 AxisAngleRotation3D, 258 B BarrelButton, 163 BasedOn, свойство, 387 BeginStoryboard, тег, 279, 286 BezierSegment, 220 BitmapImage инкапсуляция функций обработки изображений, 240 обогащенное отображение, 340 отображение с помощью элемента ContentControl, 339 BitmapMetadata, класс, 240 BlockedCommand, команда, 359 Border, элемент как элементрисовальщик, 130 обзор, 170 с кистью LinearGradientBrush, 227 создание шаблона для элементов с рамкой, 134 BorderLayout, класс, 192 Bottom, свойство класса Canvas, 188 Button, элемент управления в окне, 41 задание содержимого, 125 и шаблоны, 131 Предметный указатель 414 иерархия, 138 как составной элемент, 130 на панели инструментов, 149 обзор, 137 поддержка нажатия, 147 привязка шаблона, 135 размещение внутри StackPanel, 42 три принципа, 124 ButtonChrome, 126 C C# Visual C# Express, 59 компилятор, 38 CanExecute, метод, 353, 360 CanGoBack, свойство, 109 Canvas, менеджер размещения, 188 CaretPosition, 156 Center, свойство, 225 CER (область ограниченного выполне ния), 73 CheckBox, элемент управления, 137 ClearType, 218 Click, событие, 147, 346 Closed, событие Window, 86 Closing, событие Window, 86 CollectionViewSource, класс, 332, 336 ColumnDefinitions, свойство, 197, 203 COM, 83 CombinedGeometry, 221 ComboBox, элемент управления, 139 Command, свойство InputBinding, 408 и DataTrigger, 363 команды, привязанные к данным, 357 CommandParameter, свойство, 357 ComponentResourceKey, класс, 378 ConstantAttenuation, свойство, 250 ContentPresenter, элемент управления, 126, 377 ContentRendered, событие Window, 86 ContentTemplate, свойство, 127, 306 ContentTemplateSelector, свойство, 127 ControlTemplate добавление триггеров к элементам управления, 365, 375 интеграция анимации, 286 настройка ListBox, 142 определение, 132 создание нового, 132 сравнение с DataTemplate, 322 CopyToOutputDirectory, 81 CroppedBitmap, класс, 209 CS_PARENTDC, 216 CSS (каскадные таблицы стилей) задание стилей тегов, 367 кодирование стилей, 372 настройка элементов управления в HTML, 131 CWnd, 86 D DashStyle, свойство, 232 DataTemplateKey, свойство, 374 DataTemplateSelector, класс, 324 DataTrigger добавление триггеров к данным, 361 добавление триггеров к элементам управления, 365 определение, 361 DataType, свойство, 330 Deactivated, событие Window, 87 DeclareChangeBlock, метод, 159 DefaultValue, метаданные, 405 DependencyObject, класс, 403 DependencyProperty, 309, 403 Deployment, тег, 115 DirectionalLight, свойство, 258 DirectoryInfo, 327 DispatcherUnhandledException, 74 DispatcherUnhandledExceptionEventArgs, 74 Dock, свойство, 156, 193 DockPanel, 146, 192 DocObject, объект ActiveX, 119 DoubleAnimation, 54, 277, 285 DoubleAnimationUsingKeyFrames, 285 DrawingBrush, 229, 293 DrawingGroup, 234 DrawingImage, класс, 237 DynamicResource, 337 E ElementName, свойство, 303 Ellipse, элемент управления, 130 EmbeddedResource, 80 EventTrigger добавление к элементам управления, 364 Предметный указатель интеграция анимации с ControlTemplate, 286 определение, 361 Execute, команды, 353 Exit, команда, 354 Expander, элемент управления, 150 Expression Blend, программа, 59 F FamilyNames, свойство, 266 FamilyTypeface, свойство, 265 FieldOfView, свойство, 250 FillRule, свойство, 221 FindResource, метод, 280 FlowDocument обзор, 262 управление размещение колонок, 272 FlowDocumentPageViewer, элемент уп равления, 165 FlowDocumentReader, элемент управле ния, 165, 263 FlowDocumentScrollViewer, элемент уп равления, 165, 262 FocusManager, класс, 411 FormatConvertedBitmap, 239 Frame, элемент управления, 166 FrameworkElement задание ограничений на размещение, 180 определение, 180 свойства, описывающие размещение, 188 FrameworkElement.SetResourceReferenc e, метод, 302 FrameworkElementFactory, класс, 132, 301, 322 FrameworkPropertyMetadata, класс, 407 G GDI, система координат, 217 GeometryCombineMode, свойство, 221 GeometryDrawing, класс, 234, 249 GeometryGroup, 221 GeometryModel3D, 249, 252 GetContentStream, 83 GetDefaultView, статический метод, 332 GetName, метод, 112, 113 GetRemoteStream, 83 GetResourceStream, 83, 84 415 GoBack, метод, 109 GradientOrigin, свойство, 225 Grid, элемент управления, 197 UniformGrid, 196 гибкая модель вычисления размеров, 199 изменение во время выполнения с по мощью GridSplitter, 206 обобществление информации о раз мере, 201, 338 размещение, 203 GridSplitter, элемент управления, 206 GridView, 143 GroupBox, элемент управления, 150 H Handled, свойство, маршрутизируемые события, 351 Handled, флаг, 74 HeaderedContentControl, 150 HeaderedItemsControl, класс, 146, 148 Height, свойство ограничения на размещение, 180 управление размером окна, 91 HelloBrowser, программа, 115 HierarchicalDataTemplate, класс, 330 HorizontalAlignment, свойство, 181 HostInBrowser, атрибут, 64, 115 HTML изменение внешнего вида элементов, 130 популяризация идее навигации, 96 сравнение с XAML, 29 HWND, 86 I ICC (International Color Consortium), цветовые профили, 224 ICommand, интерфейс, 353 ICustomTypeDescriptor, интерфейс, 316 Image, класс, 237, 339 ImageBrush, 229 ImageSource, класс обзор, 237 основы изображений, 237 отображение, управляемое данными, 339 производные подклассы, 237 Предметный указатель 416 свойство Metadata, 240 InitializeComponent, метод, 45, 84 InkCanvas, элемент управления обзор, 164 определение, 153 применение, 160 INotifyCollectionChanged, интерфейс, 313, 316 INotifyPropertyChanged, интерфейс, 311, 316 InputBinding, класс, 408 IsChecked, свойство, 137 ISF (ink serialized format), формат хра нения, 160 IsReadOnly, свойство, 405 IsSharedSizeScope, свойство, 202 IsSynchronizedWithCurrentItem, свой ство, 331 IsThreeState, свойство, 137 IStorage, интерфейс, 83 IStream, интерфейс, 83 Items, свойство добавление данных в список, 139 и элемент TreeView, 145 обзор, 128 ItemsPanel, 140 ItemsPanelTemplate, 140 ItemsPresenter, 129 ItemsSource, свойство иерархическое связывание, 326, 328, 330 представления набора, 338 привязка к списку, 309 ItemTemplate, свойство, 323 IValueConverter, интерфейс использование в командах, привязан ных к данным, 359 применение для привязки, 304 J Java, реализация стыковки, 192 K KeepAlive, свойство, 112 KeepTogether, свойство, 267 KeepWithNext, свойство, 267 KeyboardNavigation, класс, 411 KeyGesture, класс, 408 L LayoutTransform, свойство, 183 Left, свойство класс Canvas, 188 позиционирование и задание размера окна, 91 LinearDoubleKeyFrame, 285 LinearGradientBrush, 225 LineHeight, свойство класса Paragraph, 267 LineJoin, свойство, 231 LineSegment, 219 ListItem, класс, 267 ListOpenWindows, обработчик события, 93 ListView, элемент управления, 142 LoadCompleted, событие, 105 LoadComponent, метод, 84 Loaded, событие Window, 86, 106 LookDirection, свойство, 250 M MappingMode, свойств кисти, 226 MarkerStyle, свойство списка, 268 MatrixCamera, 258 MaxHeight изменение размера окна пользовате лем, 91 размещение по окружности, 210 свойство менеджера размещения GridLayout, 204 Maximum, свойство, 151 MDI (многодокументный интерфейс), 67 MeasureOverride, метод, 187 MediaClock, 290 MediaElement воспроизведение аудио, 291 воспроизведение видео, 292 MediaPlayer, 290 MediaTimeline, 290 MenuItem, элемент управления, 146 MergedDictionaries, свойство объекта ResourceDictionary, 379 MeshGeometry3D, класс, 252 Microsoft Foundation Classes (MFC), 86 MinHeight изменение размера окна пользовате лем, 91 Предметный указатель ограничения на размещения, 180 свойство менеджера размещения GridLayout, 204 Minimum, свойство, 151 MinOrphanLines, свойство, 267 MinWidowLines, свойство, 267 MouseEnter, событие, 278, 281 MSBuild, файл проекта, 39 MultiDataTrigger, 361 MultiTrigger, 361 MyFontAnimation, класс, 276 N NavigateUri, свойство, 101 NavigationService, объект добавление гиперссылки на страницу, 98, 99 контроль страницы, 106 передача состояния между страница ми, 105 предотвращение закрытия окна поль зователем, 106 управление журналом, 108 navigationState, аргумент, 105 NavigationWindow, класс, 97 NewWindowClicked, обработчик собы тия, 93 NormalPressure, свойство, рукописный ввод, 163 O ObservableCollection<T>, класс, 139, 313 OpacityMask, свойство, 245 OPC (открытое соглашение о пакетах), 83 OpenCommand, 359, 362 OpenType, формат шрифтового файла, 265, 273 Orcas, 59 Orientation, свойство, 191 OriginalSource, свойство, маршрутизи руемые события, 351 OrthographicCamera, 258 OutOfMemoryException, 73 Owner, свойство окна, 89 P PageFunction, класс инновация в схеме навигации, 114 страница SayHello, 111 417 страница приветствия, 110 Panel, класс менеджеров размещения, 130 PasswordBox, элемент управления, 152 PerspectiveCamera, 258 PointLight, свойство, 258 Popup, элемент управления, 172 Positions, свойство класса MeshGeometry3D, 253 освещение трехмерной сцены, 249 PresentationHost, 120 Primitives, пространств имен, 172, 196 ProgressBar, элемент управления, 151 Properties, словарь свойств, 75, 101 PropertyMetadata, класс, 405 Q QuadraticBezierSegment, 220 R RadialGradientBrush, 225 RadioButton, элемент управления, 138, 347 RadioButtonList, 139, 145 RadiusX, свойство, 225 RadiusY, свойство, 225 Range, свойство источника освещения, 250 Rectangle, 130 RelativeToBoundingBox, 226 RemoveBackEntry, метод, 109 RenderTargetBitmap, класс, 242 RenderTransform, свойство, 183 RepeatBehavior, свойство, 284 RequestNavigate, событие, 102 ResizeBehavior, свойство, 208 ResizeDirection, свойство, 207 ResizeMode, 91 ResourceDictionary, класс, 379, 382 RichEdit, элемент управления, Win32, 124 RichTextBox, элемент управления, 152, 156 отображение видео, 292 просмотр документов, 165 сравнение с TextBox, 152 Right, свойство класс Canvas, 188 RotateTransform, класс, 284, 218 Предметный указатель 418 S ScaleTransform, класс, 218 scRGB, 224 ScrollBar, элемент управления, 151, 391 ScrollViewer, элемент управления, 172, 192 SDI (однодокументный интерфейс) обзор, 67 окна и диалоги, 86 оконная модель, 68 SelectTemplate, метод, 324 SetBinding, метод, 303 SettingsBase, класс, 77 SharedSizeGroup, свойство, 202 Show, метод, 88 ShowDialog, метод, 88 Shutdown, метод вызов, 353 управление временем жизни процес са, 72 ShutdownMode, свойство, 92 Slider, элемент управления, 151, 405 SolidColorBrush, 224 Source, свойство изображения, управляемого данными, 239 маршрутизируемого события, 351 представления набора, 337 привязки, 321 Sparkle, 59 SpotLight, свойство, 258 SpreadMethod, свойство градиентной кисти, 226 sRGB, 223 STA, потоковая модель, 393 StackOverflowException, исключение, 73 StackPanel, менеджер размещения, 130 обзор, 191 сравнение с DockPanel, 192 StartIndex, свойство списка, 268 Startup, событие, время жизни процесса, 72 StartupUri, свойство, 101 STAThread, атрибут, 393 Stretch, свойство изображения, 237 StretchDirection, свойство, 174 Stroke, класс, рукописный ввод, 160 Style.Triggers, 375 SynchronizationContext, класс, 394 System.Collection, пространство имен, 295 System.Collections.IDictionary, интер фейс, 75 System.Configuration, пространство имен, API, 76 System.IO.Packaging, пространство имен, 83 System.Windows.Window, класс, 86 T TabControl, элемент управления, 150 Table, блочный элемент, 153 TargetType, свойство, 373 TemplateBinding, 377 TextBlock, элемент управления включение текста, 260 добавление содержимого в дерево отображения, 128 контейнер для одного абзаца, 262 определение, 126 проблема при размещении внутри StackPanel, 191 TextBox добавление в окно, 43 обзор, 152 обновление данных, 312 привязка к свойству Content, 303 привязка к свойству FontFamily, 303 сравнение с RichTextBox, 156 TextEffects, свойства, 287 TextIndent, свойство класса Paragraph, 267 TextPointer, объекты, 155, 157 TextRange, объект, 155 ThemeInfoAttribute, атрибут, 379 ThreadAbortException, 73 ToolTipService, 167 Top, свойство класс Canvas, 188 ToString, метод, 128 TransformGroup, класс, 184 TranslateTransform, преобразование в трехмерной графике, 259 определение, 218 размещение, 283 TreeView, элемент управления, 144 TriangleIndices, свойство, 249, 253 Triggers, свойство, 370 TrueType, формат шрифтового файла, 265 TypeConverter, класс, 128, 304 Предметный указатель 419 U векторная графика, 26 геометрические преобразования, 26 и User32, 23 инструменты для построения прило жения, 59 интеграция, 50 композиция, 28 менеджеры размещения, 42 модель программирования XAML, 32 начало работы, 38 обзор, 23 работа с данными, 47 разметка, 40 разработка Webприложений, 29 события, 45 стили, 57 элементы управления, 41 WrapPanel, менеджер размещения, 43, 195 WritableBitmap, класс, 242 UIElement, класс, 127, 179 Unicode, кодировка, 261 UniformGrid, менеджер размещения, 196, 208 UpdateSourceTrigger, свойство, 312 URI в языке XAML, 84 приложения в стиле Web, 66 ссылка на ресурсы, 79 User32, 23 HWND, 26, 86 и Windows Forms, 25 отображение меню, 146 отсутствие вложенных контейнеров, 26 программа «Здравствуй, мир», 23 система отсечения, 23, 216 V VerticalAlignment, свойство, 181 VideoDrawing, класс, 292 ViewBase, класс, 144 Viewbox, элемент управления, 156 Visibility, свойство, 88, 179 Visual C# Express, 59 Visual Studio, 59 VisualBrush, 51, 229 W Web приложения, 65 разработка, 29 стили, 367 Width, свойство, 91 Win32, 123, 125 Windows Explorer, 192 Windows Forms настройка элементов управления, 130 поддержка MDI, 69 поддержка стыковки, 192 проблема вложенности, 26 рисование с отсечением, 25 сравнение с WPF, 25 формы, 86 Windows Software Development Kit, 59 Windows XP, 140 WPF (Windows Presentation Foundation) X x:Class, атрибут, 62 x:Name, атрибут, 62 XAML (Extensible Application Markup Language), 32 встроенные расширения, 35 импорт WPF, 37 как сценарный язык для CLR, 32 кодирование стилей, 372 независимая разметка, 121 присоединенные свойства, 37 сравнение с HTML, 29 XBAP ( XAML Browser Application) независимая разметка, 121 обзор, 114 преобразование в персональное при ложение, 63 приложение PresentationHost, 120 программа HelloBrowser, 115 Xml, свойство класса TextRange, 156 XmlAttributeNode, 317 XmlDataProvider, 320 XmlDocument, 321 XmlElement, 321 xmlns, атрибут, 33 XPath, 316 XPS (XML Paper Specification), 264 XTilt, свойство, 163 Предметный указатель 420 Y YTilt, свойство, 163 Z ZipPackage, 83 Zиндекс, 186 А Абзацы и элемент TextBlock, 262 модель FlowDocument, 262 правила вложенности элементов, 153 форматирование, 267 Автоматический выбор размера, 180 Алгоритмическое размещение, 208 вычисление предпочтительного раз мера, 208 подгонка под располагаемый размер, 210 пример, 209 установка потомков, 211 Альфаканал цвета, 224, 244 Анимация, 274 AnimationClock, 277 время и временная шкала, 282 и интеграция, 56, 286 инкапсуляция зависимости значений свойств от времени, 276 определение, 283 предопределенная, 277 программирование вручную, 274 раскадровка, 279 с помощью класса DoubleAnimation, 277 триггеры, 281 эффекты наката, 278 Атрибуты XMLимя, 317 описание текста, 261 описание шрифтов, 265 Аудио, 290 Б Базовые службы, 393 InputBinding, класс, 411 взаимодействие с устройствами вво да, 410 диспетчеры, 398 клавиатура, мышь и стилос, 407 метаданные, 404 потоки, 393 свойства .NET, 398 система свойств, 402 фокус клавиатуры, 411 Безопасность, 120, 357 Библиотека менеджеров размещения, 188 Canvas, 188 DockPanel, 192 StackPanel, 191 UniformGrid, 196 WrapPanel, 193 Библиотека элементов управления, 137 Frame, элемент управления, 166 диапазоны, 151 кнопки, 137 контейнеры, 150 меню, 146 панели инструментов, 146 редакторы, 152, 153, 156, 160, 164 рукописный ввод, 160 текстовые данные, 153 элемент InkCanvas, 164 элемент RichTextBox, 156 элемент TextBox, 160 списки, 139 создание с помощью шаблонов, 145 элемент ListView, 142 элемент TreeView, 144 элементы ListBox и ComboBox, 139 средства просмотра документов, 165 Блочные элементы правила вложенности, 153 размещение абзацев, 267 размещение в таблицах, 269 В Векторная графика обзор, 26 трехмерная графика как частный слу чай, 248 Вентили памяти, 73 Видео, 292 Вложенность механизм композиции в WPF, 28 сравнение меню с панелью инстру ментов, 148 Предметный указатель Временная шкала, 282, 290 Время жизни процесса, 72 Время, анимация, 282 Всплывающие события, 349 Встроенное размещение, 187 Встроенные элементы текста, 153, 261 Выравнивание, 274 Г Гарнитура шрифта, 266 Геометрические примитивы двумерная графика, 219 основные, 215 рисование контура фигуры пером, 231 трехмерная графика, 249 Гиперссылки добавление на страницу, 98 между страницами Welcome и SayHello, 111 поддержка нажатия, 347 привязка к команде выхода из прило жения, 354 Градиентные кисти, 225 Графика алгоритм художника, 183 векторная и растровая, 27 развитая интеграция, 50 Группировка в представлении набора, 335 обобществление размеров, 202 Д Данные, 295 всепроникающее связывание, 296 добавление триггеров, 361 модель данных в .NET, 295 отображение, управляемое данными, 338 преобразование, 297 привязка к. См. Привязка ресурсы, 297 Двумерная графика, 215 геометрические преобразования, 218 геометрические примитивы, 219 изображения. См. Изображения интеграция, 50 кисти, 224 композитный рендеринг, 216 421 независимость от разрешающей спо собности, 217 перья, 231 прозрачность, 244 растровые эффекты, 247 рисунки, 233 фигуры, 235 цвет, 222 Двустороннее связывание, 313 Действия, 345, См. Триггеры декларативные, 348 и композиция элементов, 345 и слабая связь, 346 команды, 352 команды и привязка к данным, 357 события, 348 Декларативное программирование, 31, 70 Декларативные действия, 348 Дерево отображения добавление содержимого, 127 определение, 126 привязка с помощью шаблонов дан ных, 306 создание шаблонов, 132 сравнение ControlTemplate и DataTemplate, 322 элемента ListBox, 129 Диалоговые окна модальные, 88 оконная модель, 68 предотвращение закрытия, 106 с полосой прокрутки, 173 Диапазонные элементы управления, 151, 155 Диспетчеры, 394, 398 Диффузные материалы, 257 Документы и текст, 260 дополнительные типографические средства, 273 рисунки и плавающие объекты, 271 списки, 267 таблицы, 269 форматирование абзацев, 267 форматирование на уровне страниц и колонок, 272 Дочерние элементы влияние zиндекса на размещение, 186 внутри элемента Border, 170 и элемент ScrollViewer, 172 Предметный указатель 422 контроль участия в размещениях с помощью свойства Visibility, 179 определение, 129 растягивание с помощью Viewbox, 174 Древовидная иерархия, 233 Ж Жесты класс InputBinding, 408 распознавание в InkCanvas, 164 Журнал определение, 97 опрос, 109 управление, 107 З Заякоренные блоки, 271 Здравствуй, мир (программа) в User 23 в Windows Forms, 24 и потоковая модель STA, 393 написание масштабируемых прило жений, 62 текстовая версия, 260 трехмерная версия, 249 Зеркальные материалы, 257 И Иерархическое связывание, 326 вложенные шаблоны данных, 328 класс HierarchicalDataTemplate, 330 решение проблемы копирования, 288 Излучающие материалы, 328 Измерения этап, двухэтапное размеще ние, 257 Изображения, 237 класс ImageSource, 237 метаданные, 240 создание, 242 Изолированная модель размещения, 166 Именованные ресурсы, и стили, 378 Инкрементная настройка, 136 Интеграция анимации, 286 средств WPF, 50 Интегрированная модель размещения, 166 Исключения, не допускающие восста новления, 73 Исполнение приложений в браузере, 114 HelloBrowser, 115 взгляд со стороны PresentationHost, 120 независимая разметка, 121 Источники освещения, 249 точечный, 250 Источники освещения, трехмерная гра фика, 257 К Камеры, трехмерная графика определение, 258 создание, 250 Каре, 156 Каскадные таблицы стилей. См. CSS Кисти, 224 градиентные, 225 мозаичные, 229 определение, 215 сочетание с перьями, 231 трехмерный аналог, 249 Клавиатура взаимодействие с устройствами вво да, 410 структурирование в WPF, 408 структурирование с помощью класса InputBinding, 408 фокус, 411 Клавиатурные аккорды, 408 Колонки, 272 Команды, 352 безопасные, 357 именование желаемого действия с помощью, 353 маршрутизируемые, 356 определение с помощью интерфейса ICommand, 353 привязка к данным, 355 Команды рисования, 215 Композитная система, 216 Композитный рендеринг, 216 Композиция элементов для действий, 345 для стилей, 367 для шаблонов, 134 для элементов управления, 123, 126 Контейнеры, 28, 150 Контекстное меню, отображение, 146 Контроль данных, 106 Предметный указатель Л Лигатуры, 273 Логический пиксель, 217 М Манифест сборки, 115 Материалы, трехмерная графика, 249, 257 Меню дополнительное, 149 изображения, 240 обзор, 146 привязка к команде выхода из прило жения, 354 сравнение с панелью инструментов, 146 Метаданные базовые службы, 404 Многопоточность, 394 Модели и стили, 376 трехмерные, определение, 249 трехмерные, создание сложных, 252 Модель содержимого, 125 менеджеры размещения, 130 назначение, 47 рисовальщики, 130 свойства Child и Children, 129 свойство Items, 128 создание шаблона для Window, 136 составные элементы, 130 элемент ContentPresenter, 126 Модельвидконтроллер, паттерн, 376 Мозаичные кисти, 229 Мультимедиа, 289 Мышь взаимодействие с устройствами вво да, 410 обзор, 407 события, 346 структурирование с помощью класса InputBinding, 408 эффект наката при анимации, 278 Н Навигация NavigationWindow как узел, 97 в стиле Web, 65 журнал, 100, 107 423 начальный вид окна, 99 однодокументный интерфейс, 68 передача состояния между страница ми, 101 управление, 106 функциональная, 109 Наследование стилей, 386 Начальное поведение окна, 91 Независимая разметка, 121 Независимость от разрешающей способ ности, 217 О Обличья, 381 Обобществление информации о разме ре, 201 Обработчики событий связывание разметки и файла с ко дом, 45 создание шаблона элемента управле ния, 132 Объекты CLR именование компонентов, написан ных на XAML, 84 привязка к, обзор, 308 привязка к, редактирование, 311 Ограничения на размещение, 180 Окна, 84 задание размера и положения, 91 и объект Application, 92 и разметка, 40 отображение, 88 создание в WPFприложении, 38 создание шаблона для, 136 Оконные модели, 68 Опорные кадры, анимация, 284 Отмена операций, 159 Отсечение в User32 и GDI, 216 и механизмы размещения, 183 обзор, 25 П Пакеты, модель OPC, 83 Панели инструментов, 146, 148 Перенос слов, 274 Персональные приложения добавление навигации, 66 обзор, 67 424 преобразование приложений для бра узера в, 64 развертывание, 65 Печать документов и текста, 264 Пиксели, 217 Плавающие объекты, 271 Поля, 181, 189 Потоки, 393 диспетчер, 394, 398 класс SynchronizationContext, 394 многопоточность, 396 модель STA, 393 Предпочтительный размер, двухэтапное размещение, 178 Представления наборов, 331 группировка, 335 описание в разметке, 336 сортировка, 335 управление текущим элементом, 331 фильтрация, 333 Преобразование геометрические двумерная графика, 218 при размещении, 183 трехмерная графика, 259 Преобразования геометрические, 26 данных, 297 Приведение значений, 405 Привязка иерархическая, 326 к XML, 316 к объектам, обзор, 308 к объектам, редактирование, 311 к ресурсу, 301 команд, 355 определение, 302 повсеместность, 296 шаблона, 134 элемента управления, 47 Привязки к конфигурационному файлу, настройка, 77 Приложения в стиле Web, 65 инструменты для построения, 59 масштабируемые, 61 навигация. См. Навигация обзор, 61 объект Application. См. Application, объект окна. См. Окна Предметный указатель передача состояния между страница ми, 101 персональные, 67 пользовательские элементы управле ния, 93 принципы организации, 61 Приложения, выполняемые в браузере добавление навигации, 65 преобразование в персональное при ложение, 64 Присоединенные свойства, 37, 188 Притяжение, и элемент TextBox, 157 Прозрачность двумерная графика, 244 цвета, 224 Пространства имен, XAML, 33, 36 Прямые события, 349 Р Развертывание, масштабируемое, 64 Развитое содержимое, 123, 134 Разметка ассоциирование кода с, 45 и шаблоны, 133 конструирование программ с по мощью, 40 написание простых WPFприложе ний, 61 независимая, 121 объявление привязки, 303 описание представлений наборов, 336 определение приложения в виде, 70 развертывание, 64 расширение, 34, 36 Размещение zиндекс, 186 адаптация к содержимому, 178 в сетке. См. Grid двухэтапное, 178 контракт, 178 менеджеры, 130 модель слотов, 181 нестандартное, реализация, 208 ограничения на, 180 отсутствие встроенного, 187 преобразования, 183 согласованное, 187 Размещение по окружности, 209 вычисление предпочтительного раз мера, 210 Предметный указатель как пример алгоритмического разме щения, 209 установка дочерних элементов, 211 Разреженное хранилище, 402 Раскадровка интеграция анимации с текстом, 289 обзор, 279 описание анимации с помощью, 283 управляющие действия и триггеры, 365 Располагаемый размер, двухэтапное размещение, 178 Растровая графика обзов, 26 основы, 237 поддержка кадров, 239 создание изображений, 242 Растровые эффекты, 247 Редактирование двустороннее связывание, 313 привязка к объектам CLR, 311 Редакторы. См. Библиотека элементов управления, редакторы Ресурсы загрузка, 83 и открытое соглашение о пакетах, 83 и стили, 375, 378 использование более одного раза, 301 конфигурирование, 80 обзор, 48 определение, 79, 297 путь поиска, 299 статическое присваивание или дина мическое связывание, 301 типы, 80 Рисовальщики, 170 Рисунки, 215, 233, 271 С Связи (модель OPC), 83 Сглаживание, 217 Серверное программирование, 32 Сетка, трехмерная графика, 252 Слабая связь, 347 Слоты, модель, 181 Смещения, RichTextBox, 157 События извещение об изменениях свойств, 311 обзор, 348 425 слабая связь, 346 События ввода, 349 Совмещение с пикселями, 218 Соглашения об именовании компонентов, написанных на XAML, 84 файлов с кодом, 45 Сортировка в представлении набора, 335, 337 Составные элементы управления определение, 94 поддержка шаблонов. См. Шаблоны привязка с помощью шаблонов дан ных, 306 привязка свойства TextBox к, 304 применение для вывода растрового изображения, 339 Состояние документ, 85 передача между страницами, 101 сводящееся к конфигурации, 76 связанное с содержимым, 79 управление, 75 Списковые элементы управления, 139 ListView, 142 TabControl, 151 TreeView, 144 в .NET, 295 создание с помощью шаблонов, 145 Средства просмотра документов, 165 Стандартный код, 70 Стили, 367 введение, 372 единообразие, 390 и темы, 379, 389 именованные, область действия, 368 как унифицированная модель настра ивания, 369 композиция элементов, 367 модели и отображение, 376 наследование, 386 обзор, 57 обличья, 381 оптимизация для инструментальных средств, 371 принципы, 367 советы по применению, 388 формулирование идеи, 391 Стилос взаимодействие с устройствами вво да, 410 Предметный указатель 426 пакеты, 162 структурирование с помощью класса InputBinding, 408 Страницы задание размера при форматировании документа, 272 и функциональная навигация, 109 передача состояния между, 101 Структурированное хранилище, 83 Сценарии, 31 Т Табличная верстка, 269 Текст данные, 153 интеграция с анимацией, 287 как способ представления информа ции, 260 потоковая модель, 153 размещение, 177, 266 абзацы, 267 дополнительные типографические средства, 273 рисунки и плавающие объекты, 271 сложности реализации, 177 списки, 267 страницы и колонки, 272 таблицы, 269 шрифты, 265 Текстура, 254 Темы единообразие, 390 обличья, 381 определение, 367 отделение от приложения, 390 формулирование идеи, 391 Тесселяция, 252 Типографические средства, 273 Треугольники, трехмерная графика, 252 Трехмерная графика, 248 анимация, 54 источники освещения, 257 камеры, 258 модели, 252 преобразования, 259 Триггеры в анимации, 281 добавление к данным, 361 добавление к элементам управления, 364 обзор, 361 применение стилей к, 370 резюме, 365 Туннельные события, 349 У Управление текущим элементом, 331 Устройства ввода, взаимодействие с, 410 Ф Файл с кодом, 45 Фактический размер, двухэтапное раз мещение, 178 Фигуры двумерная графика, 235 определение, 215 Фильтрация, 333 Форматирование диапазонов, 155 Функциональная навигация, 109 вызов метода SayHello, 111 конструирование пользовательского интерфейса, 110 поток управления, 112 реализация GetName, 113 создание страницы приветствия, 110 Ц Цвет, двумерная графика, 222 Ч Части (модель OPC), 83 Частичные типы, 45 Ш Шаблоны. См. Шаблоны данных всплывающие подсказки, 169 и стили, 376 повторное использование в списке изображений, 342 привязка, 134, 296 применение, 136 списковые элементы управления, 140 Шаблоны данных выбор, 324 добавление содержимого в дерево отображения, 127 Предметный указатель добавление триггеров к данным, 361 иерархическая привязка, 326 команды, привязанные к данным, 359 связывание с помощью, 321 Шрифты FontFamily, 266, 303 обзор, 265 Э Элемент данных, 302 Элементы управления. См. также Биб лиотека элементов управления Border, 170 ScrollViewer, 172 Thumb, 169 ToolTip, 167 ViewBox, 174 427 всплывающие события, 350 добавление стилей, 57, 376, 391 добавление триггеров, 364 как двумерные рисунки, 51 как трехмерные объекты, 53 код для взаимодействия, 44 модель содержимого, 125 настройка внешнего вида. См. Шаб лоны перекрытие, 25 пользовательские, 93 привязка к данным, 47 размещение. См. Размещение создание тем, 380 три принципа, 123 фокус клавиатуры, 411 Этап установки, двухэтапное размеще ние, 178 Книги издательства «ДМК Пресс» можно заказать в торгово-издатель ском холдинге «АЛЬЯНС-КНИГА» наложенным платежом, выслав открытку или письмо по почтовому адресу: 123242, Москва, а/я 20 или по электронному адресу: post@abook.ru. При оформлении заказа следует указать адрес (полностью), по которому должны быть высланы книги; фамилию, имя и отчество получателя. Желательно также указать свой телефон и электронный адрес. Эти книги вы можете заказать и в Internet-магазине: www.abook.ru. Оптовые закупки: тел. (095) 258-91-94, 258-91-95; электронный адрес abook@abook.ru. Крис Андерсон Основы Windows Presentation Foundation Главный редактор Мовчан Д.А. dm@dmk press.ru Перевод Слинкин А.A. Литературный редактор Готлиб О.В. Верстка Татаринов А.Ю. Дизайн обложки Мовчан А.Г. Подписано в печать 19.03.2007. Формат 70Ч100 1/16 . Гарнитура «Петербург». Печать офсетная. Усл. печ. л. 55,7. Тираж 1000 экз. № Электронный адрес издательства: www.dmk press.ru