Текст
                    Чарльз Петцольд
Microsoft
E
n Dj J

Microsoft’ Ж W
WINDOWS’
PRESENTA
FOUNDAffl________
БАЗОВЫЙ КУРС
' ц"
Я, РШШ hJIHH
С^ППТЕР

Charles Petzold APPLICATIONS = CODE +MARKUP A GUIDE TO THE MICROSOFT® WINDOWS® PRESENTATION FOUNDATION Microsoft Press
Чарльз Петцольд Microsoft’ WINDOWS' PRESENTATION FOUNDATION БАЗОВЫЙ КУРС КШСШЩЩП ПИТЕР Москва * Санкт-Петербург Нижний Новгород * Воронеж Ростов-на-Дону - Екатеринбург * Самара * Новосибирск Киев * Харьков - Минск 2008
ББК 32.973.2-018.2 УДК 004.451 П31 Петцольд Ч. П31 Microsoft Windows Presentation Foundation: — M.: Издательство «Русская Редакция»; СПб.: Питер, 2008. — 944 с.: ил. ISBN 978-5-7502-0341-3 («Русская Редакция») ISBN 978-5-91180-643-9 («Питер») В этой книге, написанной гуру Windows-программирования Чарльзом Петцольдом, па- раллельно изложены два подхода программирования, на которых базируется Windows Presentation Foundation (WPF). При чтении читатель будет циклически переходить от особенностей приме- нения XAML (Extensible Application Markup Language) к тонкостям программирования на С#, что позволит понять их взаимосвязанность при программировании интерфейсов. Уже с первой главы автор объясняет синтаксис XAML и принципы создания взаимосвязанного кода. Множество наглядных примеров помогут легко понять, как связываются коды на XAML и С#. Все это плюс «фирменное» разъяснение Петцольда концепций пользовательских интер- фейсов (UI) легко научит Windows-разработчиков тому, как создавать для своих приложений интерфейсы нового поколения. ББК 32.973.2-018.2 УДК 004.451 Подготовлено к изданию по лицензионнному договору с Microsoft Corporation, Редмонд, Вашингтон, США. Microsoft, Microsoft Press, Authenticode, DirectX, Internet Explorer, Tahoma, Verdana, Visual C#, Visual Studio, Win32,Windows, Windows NT, Windows Vista, WinFX и X++ являются товарными знаками или охраняемыми товарными знаками корпорации Microsoft в США и/или других странах. Все другие товарные знаки являются собственностью соответствующих фирм. Все адреса, названия компаний, организаций и продуктов, а также имена лиц, используемые в примерах, вымышлены и не имеют никакого отношения к реальным компаниям, организациям, продуктам и лицам. © ISBN 978-0-7356-1957-9 (англ.) © ISBN 978-5-7502-0341-3 («Русская Редакция») © ISBN 978-5-91180-643-9 («Питер») Оригинальное издание на английском языке, Charles Petzold, 2007 Перевод на русский язык, Microsoft Corporation, 2008 Оформление и подготовка к изданию ООО «Питер Пресс», ООО «Издательство «Русская Редакция», 2008 Чарльз Петцольд Microsoft Windows Presentation Foundation: базовый курс Серия «Для профессионалов» Перевод с английского Е. Матвеев Совместный проект издательства «Русская Редакция» и издательства «Питер» В!Н(1111Ц||||| С&ПИТЕР ООО «Питер Пресс», 198206, Санкт-Петербург, Петергофское шоссе, д. 73, лит. А29. Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 95 3005 — литература учебная. Подписано в печать 22.11.07. Формат 70x100/16. Усл. п. л. 77,4. Тираж 2200. Заказ 5806. Отпечатано по технологии CtP в ОАО «Печатный двор» им. А. М. Горького. 197110, Санкт-Петербург, Чкаловский пр., д. 15.
Содержание Введение......................................................9 Глава 1. Приложение и окно...................................14 Глава 2. Базовые кисти ......................................33 Глава 3. Концепция содержимого ..............................54 Глава 4. Кнопки и другие элементы управления.................72 Глава 5. StackPanel и WrapPanel..............................96 Глава 6. Dock и Grid........................................114 Глава 7. Canvas.............................................140 Глава 8. Зависимые свойства.................................150 Глава 9. Маршрутизируемые события ввода.....................165 Глава 10. Пользовательские элементы.........................193 Глава И. Элементы с одним дочерним объектом.................211 Глава 12. Пользовательские панели...........................243 Глава 13. ListBox...........................................266 Глава 14. Иерархия меню.....................................297 Глава 15. Панель инструментов и строка состояния ...........325 Глава 16. TreeView и ListView ..............................352 Глава 17. Печать и диалоговые окна..........................388 Глава 18. Клон Блокнота.....................................429 Глава 19. XAML .............................................475 Глава 20. Свойства и атрибуты...............................505 Глава 21. Ресурсы...........................................544 Глава 22. Окна, страницы и перемещение......................565
6 Содержание Глава 23. Привязка данных .....................................626 Глава 24. Стили................................................659 Глава 25. Шаблоны .............................................683 Глава 26. Ввод и представления данных..........................740 Глава 27. Графические фигуры ..................................780 Глава 28. Классы Geometry и Path...............................802 Глава 29. Преобразования.......................................832 Глава 30. Анимация.............................................867 Глава 31. Работа с графикой....................................904 Алфавитный указатель...........................................942
Современный цифровой компьютер изобретался и задумывался как устройство, упрощающее и ускоряющее проведение сложных, продолжительных вычислений. Однако при практическом использовании на первое место выходит способность компьютера к хранению больших объемов информации, а способность к проведению вычислений, то есть к обработке данных, во многих случаях становится почти несущественной. Никлаус Вирт. Алгоритмы + структуры данных = программы
Об авторе Чарльз Петцольд уже двадцать лет пишет книги о программировании для пер- сональных компьютеров. Его знаменитый труд «Programming Windows» вышел в пяти изданиях, оказал огромное влияние на целое поколение программистов и является одной из самых известных книг по программированию всех времен. Кроме того, его перу принадлежит книга «Code: The Hidden Language of Comp- uter Hardware and Software» — повествование о внутренней жизни умных ма- шин, получившее одобрение критиков. Чарльз является владельцем сертифика- та Microsoft MVP в области разработки клиентских приложений. Его веб-сайт находится по адресу http://www.charlespetzold.com.
Введение В этой книге рассказано об использовании платформы Microsoft Windows Pre- sentation Foundation (WPF) для написания программ, работающих в среде Micro- soft Windows. Такие программы могут быть как автономными приложениями Windows (часто называемыми клиентскими приложениями), так и интерфейсны- ми модулями для распределенных приложений. WPF считается основным интер- фейсом прикладного программирования (API) для системы Microsoft Windows Vista, но приложения WPF также запускаются в Microsoft Windows ХР с об- новлением Service Pack 2 и в Windows Server 2003 после установки Microsoft .NET Framework 3.0. Хотя WPF используется для написания программ, иногда называемых «обыч- ными приложениями Windows», они имеют мало общего с Windows-приложе- ниями предыдущего поколения. WPF означает новое визуальное оформление, новую философию настройки элементов, новые графические средства (включая анимацию и ЗВ-графику) и новый программный интерфейс. В действительности WPF состоит из двух взаимосвязанных программных интерфейсов. Программы WPF могут быть полностью написаны на C# или лю- бом другом языке программирования, компилируемом в соответствии с прави- лами .NET CLS (Common Language Specification). Кроме того, WPF содержит новый язык разметки на базе XML, называемый XAML (extensible Application Markup Language — произносится «зэмл»). В отдельных случаях на XAML мож- но написать целую программу, однако чаще приложения строятся как из про- граммного кода, так и из кода разметки. XAML обычно используется для опре- деления пользовательского интерфейса и визуального оформления приложений (включая графику и анимацию), а код обработки событий пользовательского ввода пишется отдельно. Что необходимо знать для чтения книги Предполагается, что читатель уже имеет опыт программирования на C# и в пре- дыдущих версиях .NET Framework. Если это не так, обратитесь к моей короткой книге «.NET Book Zero: What the C or C++ Programmer Needs to Know about C# and the .NET Framework». Книга распространяется свободно, ее можно загрузить на следующей странице моего веб-сайта: http://www.cha г1espetzold.com/dotnet
10 Введение Начинающему программисту я бы порекомендовал начать изучение C# с кон- сольных программ — текстовых приложений, работающих в окне командной строки. О чем рассказано в книге Я пишу программы для Windows с 1985 года и считаю WPF самым выдающимся событием в области Windows-программирования с тех времен. Однако поддерж- ка двух совершенно разных программных интерфейсов в WPF создала массу проблем при написании книги. Основательно поразмыслив, я решил, что каждый разработчик должен иметь хорошую подготовку для написания WPF-приложе- ний исключительно на уровне программного кода. По этой причине первая по- ловина книги посвящена написанию программ WPF на С#. Некоторые особенности WPF потребовали усовершенствования свойств и со- бытий .NET. Очень важно хорошо понимать суть этих усовершенствований, особенно при работе с кодом XAML. По этой причине в первой половине бы- ли выделены специальные главы, посвященные новым концепциям зависимых свойств и маршрутизируемых событий ввода. Вторая половина книги посвящена XAML. В ней будет показано, как напи- сать небольшое приложение, состоящее только из кода XAML, как объединить XAML с кодом C# для создания более крупных и серьезных приложений. Од- ной из первых задач, с которых начнется вторая половина книги, станет написа- ние служебной программы XAML Cruncher. Эта программа оказала мне огром- ную помощь при изучении XAML, и я надеюсь, вам она тоже пригодится. Так как язык XAML используется в первую очередь для определения визуального оформления, многие темы, относящиеся к работе с графикой, также были вклю- чены во вторую половину. Вероятно, в не столь отдаленном будущем большая часть кода XAML будет генерироваться интерактивными дизайнерами и другими средствами программи- рования. Уверен, что вы вскоре начнете пользоваться этими средствами, упро- щающими разработку приложений. И все же я считаю, что каждый программист WPF должен уметь писать код XAML «вручную», и постараюсь научить вас этому в книге. Возможно, вы тоже сочтете, что программирование на XAML — особенно при использовании инструментов типа XAML Cruncher — само по себе может быть весьма увлекательным занятием. Я пишу учебники, а не справочники. Читатели предыдущих книг обнаружи- ли, что книга приносит наибольшую пользу, если начать с главы 1 и последова- тельно двигаться дальше. Очень важно, чтобы ваши пальцы привыкли к вводу кода C# и XAML. Подобно навыкам жонглирования или игры на гобое, про- граммирование невозможно освоить только по книгам, без практических заня- тий. Человек быстрее и глубже изучает программный интерфейс, когда он сам вводит программный код. Попробуйте делать это, особенно в начальных главах, и не бойтесь экспериментировать. В начальных главах я предлагаю много идей для таких экспериментов.
Введение 11 Windows и программирование Компания Microsoft выпустила первую версию Windows осенью 1985 года. С тех пор система неуклонно совершенствовалась и улучшалась. Наиболее значитель- ными изменениями ознаменовался выход Microsoft Windows NT (1993) и Win- dows 95 (1995), когда система Windows перешла с 16-разрядной архитектуры на 32-разрядную. После выхода первых версий Windows существовал только один способ на- писания Windows-приложений — для работы с Windows API использовался язык программирования С. За прошедшие годы многие другие языки были адаптиро- ваны к Windows-программированию, в том числе Microsoft Visual Basic и C++. Сейчас Microsoft предлагает для языков семейства С четыре методологии напи- сания Windows-прнложеиий, представленные в следующей таблице. Программирование Windows-приложений на языках семейства С (с точки зрения Microsoft) Год Язык Интерфейс 1985 С Windows API 1992 C++ Библиотека MFC (Microsoft Foundation Classes) 2001 C# или C++ Windows Forms (часть .NET Framework 1.0) 2006 C# или C++ WPF (Windows Presentation Foundation) Я не собираюсь рассказывать вам, какой язык или интерфейс лучше исполь- зовать для написания Windows-приложений. Это решение можете принять толь- ко вы сами, руководствуясь информацией о своем задания и имеющихся ресур- сах. Но если вас интересует современное Windows-программирование (в конце концов, на дворе XXI век), выбор почти однозначен: .NET. Программы, напи- санные для платформы .NET, обычно компилируются в промежуточный код MSIL (Microsoft Intermediate Language), который затем компилируется в машин- ный код при запуске приложения. Промежуточный код защищает операционную систему от ошибок программ, а также обеспечивает платформенную независи- мость приложений .NET. Может показаться странным, что компания Microsoft выпустила две разные платформы .NET для создания клиентских приложений Windows. Я считаю, что Windows Forms все еще занимает сильные позиции в разработке Windows- приложений. Особенно с усовершенствованиями .NET Framework 2.0, Windows Forms превосходит WPF по количеству и широте возможностей элементов управ- ления и стандартных диалоговых окон, причем во многих отношениях на этой платформе проще программировать. Но хотя WPF еще уступает по ассортимен- ту элементов управления и стандартных диалоговых окон, эта платформа закла- дывает прочную основу на будущее. В частности, если вы собираетесь вносить серьезные изменения в готовые элементы или интенсивно работать с графикой — выбирайте WPF.
12 Введение Системные требования Для работы с книгой вам потребуется: О Windows Vista, Windows ХР с обновлением Service Pack 2 или Windows Server 2003. О Microsoft Visual Studio 2005 Standard Edition, Visual Studio 2005 Professional Edition, Microsoft Visual C# 2005 Express Edition или более поздняя версия Visual Studio (в Visual C# 2005 Express Edition отсутствуют значки и графи- ческие изображения, использованные в книге). О .NET Framework 3.0 (входит в поставку Windows Vista). О Пакет .NET Framework 3.0 Software Development Kit (SDK). О Компьютер, на котором могут работать все эти программы. Все версии .NET Framework и SDK доступны на сайте Microsoft: http://msdn.microsoft.com/netframework/downloads/updates Теоретически среда Visual Studio не обязательна для компиляции и запуска приложений WPF. В SDK входит программа командной строки MSBuild, строя- щая приложения WPF по файлам проектов C# (.csproj). Тем не менее Visual Studio определенно упрощает разработку WPF-приложений, а многие програм- мисты считают, что в наше время без нее не обойтись. О версии программного обеспечения Примеры книги были протестирован в .NET Framework 3.0. версии СТР (Com- munity Technology Preview) за июль 2006 года. Предполагается, что книга будет полностью совместима с окончательной версией .NET Framework 3.0 и SDK. Если все же появятся какие-либо изменения или исправления, информацию о них можно будет найти в Microsoft Knowledge Base. Благодарности За помощь в работе над книгой я хочу поблагодарить своего агента Клодетту Мур (Claudette Moore) из литературного агентства «Мур», а также своих редак- торов в Microsoft Press: руководителя проекта Валери Вули (Valerie Woolley), технического редактора Кена Скрибнера (Кепп Scribner) и всех остальных, кто постарался отполировать мой текст и исправить мои заблуждения. Огромную пользу мне принесло сообщество блоггеров (многие, но не все из которых работают в Microsoft), писавших о WPF-программировании и внесших косвенный вклад в работу над книгой, а также людей, присылавших мне реко- мендации по электронной почте. Наиболее заметную помощь мне оказали (в ал- фавитном порядке): Крис Андерсон (Chris Anderson), Пит Блуа (Pete Blois), Аарон Корнелиус (Aaron Cornelius) из www.wiredprairie.us, Пабло Ферникола
Введение 13 (Pablo Fernicola), Джессика Фослер (Jessica Fosler), Генри Хан (Henry Hahn), Карстен Янушевски (Karsten Januszewski), Чак Яджевски (Chuck Jazdzewski), Ник Крамер (Nick Kramer), Рахул Патил (Rahul Patil), Роб Рельи (Rob Relyea), Грег Шехтер (Greg Schechter), Тим Сниф (Tim Sneath), Дэвид Тейтлбаум (David Teitlebaum) и Шон Ван Несс (Shawn Van Ness). Конечно, даже при таком коли- честве безупречных помощников я все равно могу ошибаться; все ошибки, встре- чающиеся в книге, лежат исключительно на моей ответственности. Но больше всех я благодарен Дейрдре за ее слова мудрости, фразы сочувст- вия, абзацы терпения, главы великодушия и длинную, большую книгу любви. Чарльз Петцольд Нью-Йорк и Роско, штат Нью-Йорк Сентябрь 2005 — июнь 2006 гг.
Приложение и окно Жизненный цикл приложений, написанных для Microsoft WPF (Windows Pre- sentation Foundation), обычно начинается с создания объектов типа Application и Window. Простейшая WPF-программа выглядит так: SayHello.cs //--------------------------------------- // SayHello.cs (с) 2006 by Charles Petzold //--------------------------------------- using System: using System.Windows; namespace Petzold.SayHello { class SayHello { [STAThread] public static void MainO { Window win = new WindowO: win.Title = "Say Hello": win.ShowO: Application app = new ApplicationO; app.RunO; } } } Полагаю, читатель уже знаком с пространством имен System (а если нет, я бы порекомендовал прочитать электронную книгу «.NET Book Zero» на моем сай- те www.charlespetzold.com). Программа SayHello также содержит директиву using для System.Windows — пространства имен, включающего все основные классы, структуры, интерфейсы, делегатов и перечисления WPF, в том числе и клас- сы Application и Window. Другие пространства имен WPF начинаются с префикса
Приложение и окно 15 System.Windows — например, System.Windows.Controls, System.Windows.Input и System. Windows.Media. Особого внимания заслуживает System.Windows.Forms, основное пространство имен Windows Forms. Все пространства имен, начинающиеся с Sys- tem.Windows.Forms, также являются пространствами имен Windows Forms; исклю- чение составляет пространство имен System.Windows.Forms.Integration — в нем со- держатся вспомогательные классы для интеграции Windows Forms с кодом WPF. Во всех примерах, приводимых в книге, используется единая схема выбора имен. Каждая программа ассоциируется с проектом Microsoft Visual Studio. Весь код проекта изолируется в определении пространства имен; имя пространства всегда состоит из моей фамилии, за которой следует имя проекта. Так, проект первого примера называется SayHello, а, следовательно, пространство имен будет называться Petzold.SayHello. Каждый класс проекта содержится в отдельном фай- ле с исходным кодом, причем имя файла обычно совпадает с именем класса. Если проект состоит из одного класса (как в первом примере), в качестве имени класса обычно используется имя проекта. Во всех WPF-программах методу Main должен предшествовать атрибут [STA- Thread]; в противном случае компилятор C# выдает сообщение об ошибке. Дан- ный атрибут означает, что основной программный поток (thread) приложения должен использовать однопоточную (single-threaded) модель; это необходимо для взаимодействия с подсистемой COM (Component Object Model). Понятие «однопоточной модели» унаследовано из старой эпохи СОМ, предшествующей .NET. В нашем контексте это означает, что приложение не будет использовать многопоточные возможности runtime-среды. В приложении SayHello метод Main начинается с создания объекта типа Win- dow — основного класса, используемого для создания стандартного окна прило- жения. Свойство Title определяет текст заголовка окна, а метод Show отображает окно на экране. Остается сделать последний важный шаг — вызвать метод Run нового объекта Application. В парадигме Windows-программирования этот метод запускает цикл сообщений, в котором приложение получает ввод пользователя с клавиатуры и мыши. На планшетном компьютере программа также будет получать ввод со стилуса. Скорее всего, для создания, компиляции и запуска приложений, написанных для WPF, читатель будет использовать Visual Studio 2005. В таком случае про- грамма SayHello создается следующим образом. 1. Выполните команду File ► New Project. 2. В диалоговом окне New Project выберите Visual С#, Windows Presentation Foundation и Empty Project. Подберите подходящее место для хранения проекта и при- свойте ему имя SayHellp. Отключите опцию Create Directory For Solution. Щелк- ните на кнопке ОК. 3. Секция References на расположенной справа панели Solution Explorer должна включать DLL-библиотеки Presentationcore, Presentation Framework, System и Win- dowsBase. Если библиотеки отсутствуют в секции References, добавьте их — щелкните правой кнопкой мыши в области References и выберите команду Add
16 Глава 1. Приложение и окно Reference (или выберите команду Add Reference в меню Project). В диалоговом окне Add Reference перейдите на вкладку .NET, выделите необходимые библио- теки и щелкните на кнопке ОК. 4. Щелкните правой кнопкой мыши на имени проекта SayHello на панели So- lution Explorer, выберите команду New Item из меню Add (или выберите команду Add New Item в меню Project). В диалоговом окне Add New Item выберите Code File, введите имя файла SayHello.cs. Щелкните на кнопке ОК. 5. Введите программный код, приведенный ранее в этой главе, в файле SayHello.cs. 6. Выберите команду Debug ► Start Without Debugging (или нажмите Ctrl+F5), что- бы откомпилировать и запустить программу. Описанная процедура создания проекта используется в большинстве приме- ров, приводимых в первой части книги. Исключение составляют лишь отдель- ные проекты с несколькими файлами исходного кода (в том числе и один из проектов настоящей главы). При закрытии окна, созданного SayHello, на экране также обнаруживается консольное окно. Присутствием консольного окна управляет флаг компилятора, состояние которого задается в свойствах проекта. Щелкните правой кнопкой мыши на имени проекта справа, выберите в меню команду Properties (или выбе- рите команду Properties в меню Project). В открывшемся окне можно просмотреть или изменить различные аспекты проекта. Обратите внимание: в поле выходного типа (Output Туре) указано консольное приложение (Console Application). Разуме- ется, этот параметр не мешает программе выйти за пределы консоли и создать новое окно. Выберите в этом поле приложение Windows (Windows Application), и программа будет работать точно так же, но без консольного окна. На мой взгляд, консольное окно очень удобно на стадии разработки программы. Я ис- пользую его для вывода текстовой информации во время работы программы, а также в отладочных целях. Если программа оказывается настолько «кривой», что даже не создает окно (или зацикливается после его создания), приложение легко завершается нажатием Ctrl+C в консольном окне. Оба класса, использованных в SayHello — Window и Application, — являются производными от Dispatcherobject, но, как видно из следующей иерархии классов, «родословная» класса Window намного длиннее: Object Dispatcherobject (абстрактный) Application Dependencyobject Visual (абстрактный) UI Element FrameworkElement Control Contentcontrol Window Конечно, читателю пока не обязательно знать иерархию классов во всех тон- костях, но по мере знакомства с WPF эти классы будут встречаться нам снова и снова.
Приложение и окно 17 Программа может создать только один объект Application. Этот объект остает- ся невидимым; в отличие от него объект Window предстает на экране во всей красе как стандартное окно Windows. Окно обладает заголовком, текст которого задается свойством Title. В левой части заголовка расположена кнопка системно- го меню, а справа находятся кнопки сворачивания, разворачивания и закрытия окна. Рамка окна допускает изменение размеров, а все внутреннее пространство занято клиентской областью. Порядок следования команд в методе Main программы SayHello можно до оп- ределенной степени изменять — программа все равно будет работать. Например, свойство Title может задаваться после вызова Show. Теоретически исходное окно сначала отображается с пустым заголовком, но текст появляется настолько бы- стро, что никто не заметит его отсутствия. Объект Application может создаваться до создания объекта Window, но команда вызова Run должна быть последней. Метод Run возвращает управление только после закрытия окна. В этот момент метод Main завершается, a Windows осво- бождает ресурсы, использовавшиеся программой. Если исключить вызов Run, то объект Window будет создан, окно появится на экране, но будет немедленно уничтожено при выходе из Main. Вместо того чтобы вызывать метод Show объекта Window, можно передать объект Window в аргументе Run: app.Run(win); В этом случае метод Run организует вызов Show для объекта Window. Реальная работа программы начинается только с вызовом метода Run — толь- ко после него объект Window может реагировать на действия пользователя. Когда пользователь закрывает окно, а метод Run возвращает управление, программа готова к завершению. Таким образом, почти весь жизненный цикл программы проходит во время выполнения Run. Но тогда как программа может сделать что- нибудь полезное? После завершения инициализации практически все действия в программах выполняются в ответ на какое-либо событие. Самые распространенные события сообщают о вводе с клавиатуры, мыши или стилуса. В классе UIEIement (есте- ственно, сокращение UI означает «User Interface», то есть «пользовательский интерфейс») определяется ряд событий, связанных с клавиатурой, мышью и сти- лусом; класс Window наследует эти события. Одно из таких событий — Mouse- Down — происходит каждый раз, когда пользователь щелкает мышью в границах клиентской области окна. Следующая программа не только слегка изменяет порядок команд Main, но и устанавливает обработчик для события MouseDown. HandleAnEvent.cs //------------------------------------------- // HandleAnEvent.cs (с) 2006 by Charles Petzold //------------------------------------------- using System:
18 Глава 1. Приложение и окно using System.Windows; using System.Windows.Input: namespace Petzold.HandleAnEvent { class HandleAnEvent { [STAThread] public static void MainO { Application app = new Application); Window win = new WindowO; win.Title = "Handle An Event": win.MouseDown += WindowOnMouseDown; app.Run(win): } static void WindowOnMouseDown(object sender, MouseButtonEventArgs args) { Window win = sender as Window; string strMessage = string.Format("Window clicked with {0} button at point ({1})", args.ChangedButton, args.GetPosition(win)); MessageBox.Show(strMessage. win.Title): } } Обычно я назначаю имена обработчиков событий по стандартной схеме: имя начинается с имени класса/объекта, ответственного за событие, за которым сле- дует слово On и название самого события (например, WindowOnMouseDown). Впро- чем, обработчику можно назначить любое имя на ваше усмотрение. Согласно документации, обработчик события MouseDown должен определять- ся в соответствии с делегатом MouseButtonEventHandler, первый аргумент которо- го относится к типу Object, а второй — к типу MouseButtonEventArgs. Этот класс определяется в пространстве имен System.Windows.Input, поэтому в программу включается еще одна директива using. Обработчик события должен определять- ся как статический метод, потому что ссылки на него присутствуют в статиче- ском методе Main. Большинство последующих примеров в книге содержит директиву using для пространства имен System.Windows.Input, даже если это пространство в них не ис- пользуется. Событие MouseDown инициируется каждый раз, когда пользователь щелкает в клиентской области окна любой кнопкой мыши. В первом аргументе обра- ботчика события передается объект, инициировавший событие, то есть объект
Приложение и окно 19 Window. Обработчик события может безопасно преобразовать полученный объект в объект типа Window. Объект Window необходим обработчику в программе HandleAnEvent для двух целей: во-первых, он используется в качестве аргумента при вызове метода Get- Position, определяемого в классе MouseButtonEventArgs. Метод возвращает объект типа Point (структура, определяемая в System.Windows) с координатами указателя мыши по отношению к левому верхнему углу аргумента GetPosition. Во-вторых, обработчик события обращается к свойству Title объекта Window и использует его значение в качестве заголовка окна сообщения. Класс MessageBox тоже определяется в пространстве имен System.Windows. Он содержит 12 перегруженных версий статического метода Show для отображения разнообразных кнопок и изображений. По умолчанию в окне сообщения при- сутствует только кнопка ОК. Окно сообщения в программе HandleAnEvent выводит позицию указателя мыши по отношению к левому верхнему углу клиентской области. Было бы естественно предположить, что координаты задаются в пикселах, но это не так — вместо пикселов используются аппаратно-независимые единицы 1/96 дюйма. Позднее мы еще вернемся к этой необычной системе координат. Обработчик события HandleAnEvent преобразует аргумент sender в объект типа Window, однако объект Window также можно получить и другими способами. На- пример, созданный в Main объект Window можно сохранить в статическом поле, а затем использовать его в обработчике события. Кроме того, обработчик со- бытия может воспользоваться свойствами класса Application. Класс содержит ста- тическое свойство Current, которое возвращает объект Application, созданный про- граммой (как упоминалось ранее, программа может создать только один объект Application). Класс Application также содержит свойство экземпляра MainWindow, которое возвращает объект Window. Следовательно, обработчик события также может со- здать локальную переменную Window: Window win = Application.Current.MainWindow: Если единственной целью получения объекта Window было обращение к тек- сту Title для окна сообщения, то в вызов метода MessageBox.Show можно было включить аргумент Application.Current.MainWindow.Title. В классе Application также определяются некоторые полезные события. Как обычно бывает в .NET, большинство событий связывается с защищенными мето- дами, отвечающими за инициирование событий. Определяемое в Application собы- тие Startup инициируется защищенным методом OnStartup и происходит вскоре после того, как программа вызовет метод Run объекта Application. Вызов метода OnExit (и инициирование соответствующего события Exit) происходит перед воз- вратом управления методом Run. Эти два события могут использоваться для вы- полнения инициализации/деинициализации уровня приложения. Метод OnSessionEnding и событие SessionEnding указывают, что пользователь от- дал команду завершения текущего сеанса Windows или выключения компьютера.
20 Глава 1. Приложение и окно Событие передается с аргументом типа SessionEndingCancelEventArgs — класса, производного от CancelEventArgs и включающего свойство с именем Cancel. Если приложение пожелает заблокировать выход из Windows, достаточно задать этому свойству истинное значение. Программа получает событие только в том случае, если она была откомпилирована как приложение Windows, а не как консольное приложение. Если программа должна обрабатывать некоторые события класса Application, она может установить обработчики для этих событий, однако существует более удобное решение — определение класса, производного от Application, как в сле- дующем примере InheritTheApp. Класс, производный от Application, может про- сто переопределить базовые методы, ответственные за инициирование событий. InheritTheApp.cs //---------------------------------------------- // InheritTheApp.es (с) 2006 by Charles Petzold //---------------------------------------------- using System: using System.Windows; using System.Windows.Input; namespace Petzold.InheritTheApp { class InheritTheApp : Application [STAThread] public static void MainO { InheritTheApp app = new InheritTheAppO; app.RunO; } protected override void OnStartup(StartupEventArgs args) { base.OnStartup(args); Window win = new WindowO; win.Title = "Inherit the App"; win.ShowO; } protected override void OnSessionEnding(SessionEndingCancelEventArgs args) { base.OnSessionEnding(args); MessageBoxResult result = MessageBox.ShowCDo you want to save your data?", MainWindow.Title, MessageBoxButton.YesNoCancel, MessageBoxImage.Question, MessageBoxResult.Yes);
Приложение и окно 21 args.Cancel = (result == MessageBoxResult.Cancel); } } } Класс InheritTheApp наследует от Application и переопределяет методы OnStartup и OnSessionEnding, определяемые в классе Application. В этой программе метод Main вместо объекта типа Application создает объект типа InheritTheApp. С другой сто- роны, Main является членом этого класса. На первый взгляд ситуация выглядит немного странно: Main создает экземпляр класса, к которому он принадлежит. Од- нако это абсолютно нормально, потому что метод Main определен как статический. Он существует даже в том случае, если объект InheritTheApp еще не был создан. InheritTheApp переопределяет как метод OnStartup (вызываемый вскоре после вызова Run), так и метод OnSessionEnding. Именно во время выполнения метода OnStartup программа создает объект Window и отображает его. Класс InheritTheApp также может выполнять эту операцию в конструкторе. В переопределенной версии OnSessionEnding программа выводит окно сообще- ния с кнопками Yes, No и Cancel. Обратите внимание: заголовок окна сообщения заполняется текстом MainWindow.Title. Так как мы имеем дело с методом экземп- ляра класса, производного от Application, для получения свойства экземпляра достаточно простого обращения вида Main.Window. Конечно, у программы нет данных для сохранения, поэтому она игнорирует кнопки Yes и No, разрешая приложению завершиться, а системе Windows — про- должить завершение сеанса пользователя. Если пользователь выберет кнопку Cancel, программа устанавливает флаг Cancel объекта SessionEndingCancelEventArgs. Установка флага запрещает выключение компьютера или завершение сеанса. Вы- полняемое действие (выключение или завершение системы) определяется свой- ством ReasonSessionEnding объекта SessionEndingCancelEventArgs. Свойство Reason- SessionEnding принимает значения ReasonSessionEnding.Logoff или ReasonSession- Ending. Shutdown. Оба метода программы, OnStartup и OnSessionEnding, начинают свою работу с вызова метода базового класса. В нашем примере эти вызовы не являются строго необходимыми, но вреда от них не будет. Как правило, методы базового класса лучше вызывать, если только у вас нет веских причин для противного. Как известно, при запуске программ из окна командной строки им можно пе- редавать аргументы. С программами Windows дело обстоит аналогично. Для об- работки аргументов командной строки метод Main должен определяться несколь- ко иначе: public static void Main(string[] args) Все аргументы командной строки передаются Main в виде массива строк. Этот массив строк также доступен в методе OnStartup как свойство Args аргумен- та StartupEventArgs. Наличие у Application свойства MainWindow наводит на мысль, что программа может использовать несколько окон. Разумеется, это предположение истинно. Следующая программа создает на рабочем столе дополнительные окна.
22 Глава 1. Приложение и окно ThrowWindowParty.cs //------------------------------------------------- // ThrowW1ndowParty.es (с) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Windows: using System.Windows.Input; namespace Petzold.ThrowWindowParty { class ThrowWindowParty: Application { [STAThread] public static void MainO { ThrowWindowParty app = new ThrowW1ndowParty(): app.RunO; } protected override void OnStartupCStartupEventArgs args) { Window winMain = new WindowO: winMain.Title = "Main Window"; winMain.MouseDown += WindowOnMouseDown; winMain.ShowO; for (int i = 0; i < 2; i++) { Window win = new WindowO: win.Title = "Extra Window No. " + (i + 1); win.ShowO; } } void WindowOnMouseDown(object sender, MouseButtonEventArgs args) { Window win = new WindowO: win.Title = "Modal Dialog Box": win.ShowDialog(); } } } Класс ThrowWindowParty, как и InheritTheApp, является производным от Application и создает объект Window в переопределении метода OnStartup. Затем он создает еще два объекта Window, которые также отображаются на экране. (Вскоре мы подробнее обсудим, что происходит в обработчике события MouseDown.) Прежде всего следует заметить, что три окна, созданные в переопределении OnStartup, пользуются равными правами. На любом окне можно щелкнуть, и это окно выйдет на передний план. Окна можно закрыть в любом порядке, при- чем программа завершается только с закрытием последнего окна. Если бы одно
Приложение и окно 23 окно не было снабжено заголовком «Main Window», оно бы ничем не отлича- лось от двух других. Впрочем, анализ свойства MainWindow объекта Application показывает, что глав- ным окном приложения (по крайней мере изначально) считается первое окно, для которого был вызван метод Show. Класс Application также содержит свойство Windows (обратите внимание на множественное число) типа Windowcollection. Класс Windowcollection является типич- ным классом-коллекцией .NET, реализует интерфейсы ICollection и lEnumerable и предназначается (как подсказывает название) для хранения набора объектов Window. Класс содержит свойство с именем Count и индексатор для получения всех отдельных объектов Window, для которых ваша программа вызывала ме- тод Show и которые еще продолжают существовать. В конце переопределения OnStartup свойство Windows.Count возвращает значение 3, а элемент Windows[0] со- ответствует окну с заголовком «Main Window». У программы есть одна странность: все три окна отображаются на панели за- дач Windows, находящейся (у большинства пользователей) у нижнего края эк- рана. Для приложений считается дурным тоном занимать несколько позиций на панели задач. Чтобы лишние окна не отображались на панели задач, в цикл for включается следующая команда: win.ShowInTaskbar = false: Но теперь возникает другая странность. Если первым закрывается окно с за- головком «Main Window», кнопка на панели задач исчезает — однако програм- ма продолжает работать, и на экране остаются два окна! Как правило, программа завершается при возврате из метода Run, а метод Run по умолчанию возвращает управление при закрытии последнего окна. Данный аспект поведения определяется свойством ShutdownMode объекта Application, ко- торому присваивается одно из значений перечисляемого типа ShutdownMode. Вместо используемого по умолчанию значения ShutdownMode.OnLastWindow- Close можно задать значение ShutdownMode.OnMainWindowClose. Попробуйте вста- вить следующую команду непосредственно перед вызовом Run: app.ShutdownMode = ShutdownMode.OnMainWindowClose; Также можно вставить следующую команду в любую позицию переопре- деления OnStartup (в Main перед свойством должно быть указано имя объекта Application; в методе OnStartup достаточно указать только свойство, а префикс this не обязателен): ShutdownMode = ShutdownMode.OnMainWindowClose: Теперь метод Run возвращает управление (а программа завершается) при за- крытии главного окна. Попробуйте вставить в цикл for следующую команду: MainWindow = win: Напомню, что MainWindow является свойством класса Application и использует- ся программой для определения окна, считающегося главным окном программы.
24 Глава 1. Приложение и окно После завершения цикла for главным будет считаться окно с заголовком «Extra Window No. 2», и именно это окно необходимо закрыть для завершения про- граммы. У свойства ShutdownMode также существует еще одно, третье возможное значе- ние. Если задать свойство равным ShutdownMode.OnExplicitShutdown, метод Run вер- нет управление только после явного вызова метода Shutdown объекта Application. Теперь удалите весь вставленный код со свойствами ShutdownMode и Main- Window класса Application. Существует еще один способ создания иерархии окон приложения, основанный на использовании свойства Owner, определенного в клас- се Window. По умолчанию свойство равно null; это означает, что окно не имеет владельца. Вместо этого можно задать ему другой объект Window. Например, вставьте в цикл for следующую команду: win.Owner = winMain; Теперь главное окно становится владельцем двух других окон. Вы по-преж- нему можете переключаться между всеми тремя окнами, но при этом «принадле- жащие» окна всегда отображаются перед своим окном-владельцем. Если свернуть окно-владельца, то принадлежащие ему окна исчезают с экрана, а при закрытии владельца принадлежащие окна также автоматически закрываются. В сущности, два дополнительных окна превратились в немодальные диалоговые окна. Немодальные диалоговые окна составляют меньшую из двух категорий диа- логовых окон; модальные окна применяются намного чаще. Немодальные окна позволяют работать с основным приложением, пока диалоговое окно остается на экране. Модальное окно полностью перехватывает весь ввод пользователя в приложении, и дальнейшая работа с другими окнами становится возможной только после закрытия модального окна. Попробуйте сделать следующее: вернитесь к первой программе SayHello, за- мените в исходном коде вызов Show вызовом ShowDialog и закомментируйте все ссылки на объект Application. Программа все равно работает, потому что ShowDialog реализует собственный цикл сообщений для обработки входных событий. Мо- дальность диалоговых окон обеспечивается тем, что окно не участвует в цикле обработки сообщений приложения, а следовательно, не разрешает приложению получать события пользовательского ввода. Наряду с классами, производными от Application, приложения также часто определяют классы, производные от Window. Следующая программа состоит из трех классов и трех файлов с исходным кодом. Проект называется InheritAppAnd- Window — это имя также присвоено классу, содержащему только метод Main. InheritAppAndWindow.es //-------------------------------------------------- // InheritAppAndWindow.es (с) 2006 by Charles Petzold //-------------------------------------------------- using System: using System.Windows: using System.Windows.Input:
Приложение и окно 25 namespace Petzold.InheritAppAndWindow { class InheritAppAndWindow { [STAThread] public static void MalnO { MyApplication app = new MyApplicationO; app.RunO: } } } Метод Main создает объект типа MyApplication и вызывает для него метод Run. Класс MyApplication, производный от Application, определяется следующим образом. MyApplication.cs // // MyAppl1cat1on.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Input; namespace Petzold.InheritAppAndWindow { class MyApplication : Application { protected override void OnStartupCStartupEventArgs args) { base.OnStartup(args); MyWIndow win = new MyWIndowO; w1n.Show(); } } } В переопределении метода OnStartup класс создает объект типа MyWindow — третьего класса проекта, производного от Window. MyWindow.cs //------------------------------------------ // MyWindow.cs (с) 2006 by Charles Petzold //------------------------------------------ using System; using System.Windows; using System.Windows.Input;
26 Глава 1. Приложение и окно namespace Petzold.InherltAppAndWIndow { public class MyWindow : Window { public MyWIndowO { Title = "Inherit App & Window"; } protected override void OnMouseDown(MouseButtonEventArgs args) base.OnMouseDown(args): string strMessage = strIng.Format("Window clicked with {0} button at point ({1})". args.ChangedButton. args.GetPosItlon(th1s)); MessageBox.Show(strMessage, Title): } } } Классы, производные от Window, обычно инициализируются в конструкторе класса. В нашем примере инициализация сводится к заданию свойства Title. Обратите внимание: при ссылках на свойство не требуется указывать префикс имени объекта, потому что MyWindow наследует это свойство от Window. При желании можно использовать в качестве префикса ключевое слово this: this.Title = "Inherit App & Window": Вместо того чтобы устанавливать обработчик события MouseDown, класс мо- жет переопределить метод OnMouseDown. Так как метод OnMouseDown является методом экземпляра, он может передать ключевое слово this методу GetPosition для указания объекта Window, а при ссылках на свойство Title префикс не обяза- телен. Рассмотренный код абсолютно нормально работает, но на практике в WPF- программах, не содержащих разметки, чаще определяется класс, производный от Window — без класса, производного от Application. Типичная программа выгля- дит примерно так. InheritTheWin.cs //---------------------------------------------- // Inher1tTheW1n.cs (с) 2006 by Charles Petzold // using System; using System.Windows: using System.Windows.Input: namespace Petzold.InheritTheWIn { class InheritTheWin : Window
Приложение и окно 27 { [STAThread] public static void MainO { Application app = new Application(); app.Run(new InheritTheWin()); } public InheritTheWin() { Title = "Inherit the Win"; } } } Именно такая структура будет использоваться в большинстве примеров пер- вой части книга. Как видите, программа получается довольно компактной, а если вам захочется сократить метод Main до абсолютного минимума, вся его функцио- нальность умещается в одной команде: new ApplicationO.Run(new InheritTheWin()); Давайте немного поэкспериментируем. Я представлю некоторые рекоменда- ции относительно того, как изменить программу, а вы можете либо последовать моим советам, либо (еще лучше) самостоятельно опробовать другие решения. Позиция и размеры окна на экране задаются операционной системой Win- dows, но при желании вы можете вмешаться в происходящее. Класс Window на- следует от FrameworkElement свойства Width и Height, значения которых можно за- дать в конструкторе: Width =288: Height = 192: Значения свойств Width и Height изначально не определены. Если они не за- даются в программе, то так и остаются неопределенными. Следовательно, если вам когда-либо потребуется узнать реальные размеры окна, не используйте Width и Height — вместо них следует использовать свойства Actualwidth и ActualHeight, доступные только для чтения. Однако в конструкторе окна эти два свойства рав- ны 0; они получают реальные значения только при отображении окна на экране. На первый взгляд кажется, что числа в этих двух командах выбраны случайно: Width = 288: Height = 192: Значения этих свойств задаются не в пикселах, иначе свойства Width и Height не пришлось бы определять как вещественные с двойной точностью. Единицы, в которых в WPF задаются все размеры и позиции, иногда называются аппа- ратно-независимыми или логическими пикселами, но, пожалуй, во избежание пу- таницы пикселы вообще лучше не упоминать. Я буду называть их аппаратно- независимыми единицами. Каждая единица соответствует 1/96 дюйма, поэтому
28 Глава 1. Приложение и окно значения 288 и 192 в действительности указывают, что окно имеет 3 дюйма в ши- рину и 2 дюйма в высоту. Впрочем, нет гарантии, что, приложив линейку к монитору, вы получите именно эти цифры. Соотношение между пикселами и дюймами устанавливается системой Windows и может изменяться пользователем. Щелкните правой кноп- кой мыши на рабочем столе Windows и выберите в меню команду Свойства. Пе- рейдите на вкладку Параметры, щелкните на кнопке Дополнительно, а затем пе- рейдите на вкладку Общие. По умолчанию Windows устанавливает разрешение в 96 точек на дюйм; в этом случае значения 288 и 192 точно соответствуют пикселам. Но если на мониторе выставлено разрешение в 120 DPI, то ширина окна в пикселах будет равна 360, а высота — 240. Итоговый размер окна останется прежним — 3x2 дюйма. Даже когда в будущем появятся мониторы со значительно более высокими разреше- ниями, WPF-программы будут работать без изменений. Аппаратно-независимые единицы повсеместно используются в WPF. Напри- мер, некоторые программы, представленные ранее в этой главе, выводили пози- цию щелчка относительно левого верхнего угла клиентской области. Позиция задавалась не в пикселах, а в аппаратно-независимых единицах 1/96 дюйма. Если вы захотите расположить окно в определенном месте экрана, это тоже возможно — задайте свойства Left и Тор, определенные в классе Window: Left = 500: Top = 250; Эти два свойства определяют позицию левого верхнего угла окна по отноше- нию к левому верхнему углу экрана. Как и прежде, значения относятся к типу double, задаются в аппаратно-независимых единицах, и если не будут заданы в программе, то останутся неопределенными. Класс Window не определяет пар- ные свойства Right и Bottom — правая и нижняя границы определяются свойст- вами Left/Гор и размерами окна Предположим, видеоадаптер и монитор способны отображать 1600x1200 пик- селов, и именно такое разрешение указано в свойствах экрана. Вы анализируете значения статических свойств Parameters.PrimaryScreenWidth и SystemParameters. PrimaryScreenHeight. Будут ли они равны 1600 и 1200? Только если на вашем эк- ране установлен масштаб 96 DPI. Это означает, что размеры изображения со- ставляют 16 2/3x12 1/2 дюйма. Но если выбрать масштаб 120 DPI, SystemParameters.PrimaryScreenWidth и Sys- temParameters.PrimaryScreenHeight возвращают значения 1280 и 960 аппаратно-не- зависимых единиц, подразумевающие метрические размеры 13 1/3x10 дюймов. Так как SystemParameters выдает практически все размеры в аппаратно-незави- симых единицах (исключение составляют свойства SmalllconWidth и SmalllconHeight, измеряемые в пикселах), большинство значений можно использовать в вычис- лениях без каких-либо преобразований. Например, следующий фрагмент размещает окно в правом нижнем углу экрана: Left = SystemParameters.PrimaryScreenWidth - Width: Top = SystemParameters.PrimaryScreenHeight - Height;
Приложение и окно 29 Предполагается, что свойства Width и Height уже заданы. Тем не менее ре- зультат может вам не понравиться — если у нижнего края экрана отображается панель задач, она скроет нижнюю часть окна. Окно лучше разместить в правом нижнем краю рабочей области — так называется часть экрана, не занятая пане- лями рабочего стола (самым распространенным примером которых является па- нель задач). Свойство SystemParameters.WorkArea возвращает объект типа Rect — структуры, определяющей прямоугольник по координатам левого верхнего угла и размерам. Свойство WorkArea должно определяться в виде Rect, а не просто шириной и вы- сотой, потому что пользователь может разместить панель задач у левого края экрана. В этом случае свойство Left структуры Rect будет отлично от нуля, а свой- ство Width будет равно ширине экрана за вычетом Left. Код размещения окна в правом нижнем углу рабочей области выглядит так: Left = SystemParameters.WorkArea.Width - Width; Top = SystemParameters.WorkArea.Height - Height; А вот как окно размещается в центре рабочей области: Left = (SystemParameters.WorkArea.Width - Width) / 2 + SystemParameters.WorkArea.Left: Top = (SystemParameters.WorkArea.Height - Height) / 2 + SystemParameters.WorkArea.Top; Альтернативное решение основано на использовании свойства WindowStartup- Location, определяемого классом Window. Допустимые значения объединены в пе- речисление WindowStartupLocation. По умолчанию используется значение Window- StartupLocation.Manual, которое означает, что позиция окна задается программой или операционной системой Windows. Значение WindowStartupLocation.CenterScreen выравнивает окно по центру. Несмотря на имя, окно выравнивается по центру рабочей области, а не по центру экрана. (Третье значение WindowStartupLocation. CenterOwner используется для выравнивания модальных диалоговых окон по цен- тру владельца.) Следующая программа размещает свое окно в центре рабочей области и из- меняет его размеры на 10 процентов при каждом нажатии клавиши Т или Ф. GrowAndShrink.cs //--------------------------------------------- // GrowAndShrink.cs (с) 2006 by Charles Petzold //--------------------------------------------- using System: using System.Windows; using System.Windows.Input; namespace Petzold.GrowAndShrink { public class GrowAndShrink : Window { [STAThread]
30 Глава 1. Приложение и окно public static void MalnO { Application app = new ApplicatlonO; app.Run(new GrowAndShrlnkO); } public GrowAndShrlnkO Title = "Grow & Shrink"; W1ndowStartupLocatlon = W1ndowStartupLocatl on.CenterScreen; Width = 192; Height = 192; } protected override void OnKeyDown(KeyEventArgs args) { base.OnKeyDown(args): If (args.Key == Key.Up) { Left -= 0.05 * Width: Top -= 0.05 * Height: Width *= 1.1: Height *= 1.1: } else if (args.Key == Key.Down) { Left += 0.05 * (Width /= 1.1): Top += 0.05 * (Height /= 1.1); } } } } Метод OnKeyDown (и связанное с ним событие KeyDown) реагирует на нажа- тия клавиш. Каждый раз, когда вы нажимаете и отпускаете клавишу на клавиа- туре, вызываются методы OnKeyDown и OnKeyUp. Переопределяя эти методы, вы сможете обрабатывать нажатия клавиш. Свойство Key объекта KeyEventArgs содер- жит элемент большого перечисления Key и указывает, с какой клавишей выпол- нялась операция. Поскольку свойства Left, Top, Width и Height являются вещест- венными, увеличение и уменьшение окна не приводит к потере точности. Окно может достичь минимальных или максимальных размеров, установленных сис- темой Windows, но свойства все равно сохраняют вычисленные значения. Методы OnKeyDown и OnKeyUp используются для получения нажатий клавиш управления курсором и функциональных клавиш, но для ввода с клавиатуры символов Юникода следует переопределить метод OnTextlnput. Свойство Text аргумента TextCompositionEventArgs содержит строку из символов Юникода. Обыч- но строка состоит из одного символа, но системы речевого и рукописного вво- да тоже могут генерировать вызовы OnTextlnput, и полученная строка может быть длиннее.
Приложение и окно 31 Следующая программа не задает свойство Title; пользователь сам вводит за- головок окна. TypeYourTitle.cs //---------------------------------------------- // TypeYourT1tle.cs (с) 2006 by Charles Petzold И----------------------------------------------- using System; using System.Windows; using System.Windows.Input; namespace Petzold.TypeYourTitie { public class TypeYourTitle : Window { [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new TypeYourTitleO): } protected override void OnTextInput(TextCompositionEventArgs args) { base.OnTextlnput(args): if (args.Text == "\b" && Title.Length > 0) Title = Title.Substrings, Title.Length - 1): else if (args.Text.Length > 0 && !Char.IsControl(args.Text[0])) Title += args.Text: } } } Метод поддерживает единственный управляющий символ Backspace ('\b'), и только в том случае, если Title содержит хотя бы один символ. В противном случае метод просто присоединяет текст, введенный с клавиатуры, к текущему содержимому Title. Класс Window определяет ряд других свойств, влияющих на внешний вид и по- ведение окна. Свойству Windowstyle присваиваются значения из перечисляемого типа Windowstyle. По умолчанию используется значение Windowstyle.SingleBorder- Window. Значение WindowStyle.ThreeDBorderWindow придает окну более эффектный вид, но слегка уменьшает размер клиентской области. Значение Windowstyle. ToolWindow обычно используется для диалоговых окон. Полоса заголовка у та- ких окон короче, содержит кнопку закрытия, по не содержит кнопок сворачива- ния и разворачивания. Впрочем, окно все равно можно свернуть пли развернуть, вызвав системное меню клавишами Alt+пробел. Окно со стилем WindowStyle.None также имеет рамку изменения размеров, но без полосы заголовка. Комбинация
32 Глава 1. Приложение и окно Alt+пробел по-прежнему вызывает системное меню. Содержимое свойства Title не отображается окном, но присутствует в панели задач. Присутствие или отсутствие рамки изменения размеров зависит от свойства ResizeMode, принимающего значения из перечисления ResizeMode. По умолчанию используется значение ResizeMode.CanResize, при котором пользователь может изменять размеры окна, сворачивать и разворачивать его. Со значением Resize- Mode.CanResizeWithGrip в левом нижнем углу клиентской области отображается небольшой манипулятор для изменения размеров. Значение ResizeMode.CanMinimize блокирует ручное изменение размеров и кнопку разворачивания, но возможность сворачивания окна сохраняется. Этот режим полезен для окон фиксированного размера. Наконец, значение ResizeMode.NoResize блокирует любые изменения раз- меров окна. Еще одно важное свойство класса Window — Background — наследуется клас- сом Window от Control и управляет прорисовкой клиентской области. Свойство Background содержит объект типа Brush, причем для прорисовки фона могут ис- пользоваться градиентные кисти, растровые кисти, построенные на базе изобра- жений и т. д. Кисти играют настолько важную роль в WPF, что в книге им по- священы целых две главы.
Базовые кисти Основная внутренняя часть стандартного окна называется клиентской областью. Именно в клиентской области программы выводят текст, графику и элементы и получают пользовательский ввод. Вероятно, клиентские области окон, созданных в предыдущей главе, были ок- рашены в белый цвет, но только потому, что белый используется по умолчанию для закраски клиентской области. В панели управления можно задать систем- ным цветам нестандартные значения — по эстетическим причинам или чтобы подчеркнуть свою неповторимую индивидуальность. А может быть, вы принад- лежите к числу тех, кто лучше воспринимает экран с белыми объектами (напри- мер, текстом) на черном фоне. Цвет в Windows Presentation Foundation инкапсулируется в структуре Color, определенной в пространстве имен System.Windows.Media. Как принято в графи- ческих средах, для представления цветов в структура Color используются уровни интенсивности красной, зеленой и синей составляющих (R, G и В соответствен- но). Трехмерное пространство, определяемое тремя составляющими, называется цветовым пространством RGB. Структура Color содержит три байтовых свойства с именами R, G и В, доступ- ных для чтения и записи. Значения свойств лежат в интервале от 0 до 255. Если все три свойства равны 0, структура определяет черный цвет, а если все три свойства равны 255 — белый. К этим трем свойствам структура Color добавляет альфа-канал, представлен- ный свойством А. Альфа-канал управляет прозрачностью цвета; при А=0 цвет полностью прозрачен, а при А=255 он полностью непрозрачен. Промежуточные значения соответствуют разным степеням прозрачности. У Color, как и у всех структур, имеется непараметризованный конструктор, но этот конструктор создает цвет, у которого все свойства равны 0, то есть полно- стью прозрачный черный. Чтобы превратить его в видимый цвет, можно задать значения всех четырех свойств Color: Color clr = new Col or О ; clr.A = 255; clr.R = 255: clr.G = 0; clr.В = 255:
34 Глава 2. Базовые кисти Мы получаем непрозрачный малиновый цвет. Структура Color также содержит статические методы для создания цветовых объектов в одной строке кода. Первому методу передаются три аргумента типа byte: Color clr = Color.FromRgb(r. g. b) У полученного цвета свойство А равно 255. Альфа-канал также можно задать при вызове: Color clr = Color.FromArgb(a. r, g. b) Цветовое пространство RGB с байтовыми значениями красной, зеленой и синей составляющих иногда называют цветовым пространством sRGB (где s означает «стандарт»). Пространство sRGB формализует стандартные методы отображения растровых изображений со сканеров и цифровых камер на мониторах компьюте- ров. Величина составляющих sRGB обычно прямо пропорциональна напряжению электрических сигналов, передаваемых оборудованием видеоадаптера монитору. Конечно, sRGB не подходит для представления цветов на других устройст- вах вывода. Скажем, если принтер способен выдать «более зеленый» цвет, чем средний компьютерный монитор, как представить этот уровень зеленого, если максимальное значение 255 соответствует цвету на мониторе? Для решения подобных проблем были определены другие цветовые про- странства RGB. Структура Color в WPF поддерживает одну из таких альтерна- тив — цветовое пространство scRGB (ранее оно называлось sRGB64, потому что цветовые составляющие в нем представляются 64-разрядными величинами). В структуре Color составляющие scRGB хранятся в виде вещественных свойств с одинарной точностью ScA, ScR, ScG и ScB. Эти свойства связаны с A, R, G и В: изменение свойства G приводит к изменению ScG, и наоборот. Если свойство G равно 0, то и ScG равно 0. Если свойство G равно 255, то свойство ScG равно 1. Зависимость между свойствами в границах интервала не является линейной, как видно из следующей таблицы. ScG G <=0 0 0.1 89 0.2 124 0.3 149 0.4 170 0.5 188 0.6 203 0.7 218 0.8 231 0.9 243 >1.0 255
Базовые кисти 35 Аналогичные соотношения существуют между ScR и R (а также ScB и В). Зна- чения ScG могут быть меньше 0 или больше 1; это необходимо для представле- ния цветов, находящихся за пределами возможностей мониторов или числового диапазона sRGB. Современные электронно-лучевые трубки яркость (I) связана с подаваемым напряжением (V) не линейной, а экспоненциальной зависимостью: I = V\ Показатель степени Y определяется характеристиками дисплея и освещения; в обычных условиях он лежит в интервале от 2,2 до 2,5 (стандарт sRGB предпо- лагает 2,2). Визуальное восприятие яркости человеческим глазом также нелинейно — оно пропорционально примерно яркости света в степени 1/3. К счастью, нели- нейность восприятия и нелинейность ЭЛТ примерно компенсируют друг друга, так что составляющие sRGB воспринимаются линейно. Другими словами, зна- чение RGB (80,80,80) (в шестнадцатеричной записи) примерно соответствует тому, что люди называют «средним серым». С другой стороны, составляющие scRGB проектировались как линейные по отношению к яркости, поэтому зависимость между ScG и G выражается формулой G У'1 2 255 J ’ где показатель степени 2,2 — значение Y, заложенное в стандарт sRGB. Обратите внимание: отношение имеет приближенный характер, а наименьшая точность обеспечивается при малых значениях. Зависимость для альфа-канала выглядит проще: scG = 1 Л sc А =---. 255 Для создания объекта Color по цветовым составляющим scRGB используется следующий статический метод: Color cl г = Color.FromScRgb(a, г, g, b): Аргументы являются вещественными значениями, MoiyT быть меньше 0 и боль- ше 1. Пространство System.Windows.Media также содержит класс Colors (обратите вни- мание на множественное число), который содержит 141 статическое свойство, доступное только для чтения. Имена свойств начинаются по алфавиту с AliceBlue и Antiquewhite и заканчиваются на Yellow и YellowGreen. Пример: Color cl г = Colors.PapayaWhIp; Все названия цветов, кроме одного, совпадают с теми, что поддерживаются веб-браузерами. Единственное исключение составляет свойство Transparent, воз- вращающее значение Color с нулевым альфа-каналом. Остальные 140 свойств класса Colors возвращают объект Color с заранее заданными составляющими sRGB и альфа-каналом 255.
36 Глава 2. Базовые кисти Программа может изменить цвет фона клиентской области при помощи свой- ства Background, унаследованного Window от Control. Свойству Background задается не объект Color, а гораздо более гибкий и универсальный объект типа Brush. Кисти настолько широко применяются в WPF, что мы должны уделить им внимание в самом начале книги. Класс кисти Brush является абстрактным клас- сом, как видно из следующей иерархии: Object Dispatcherobject (абстрактный) Dependencyobject Freezable (абстрактный) Animatable (абстрактный) Brush (абстрактный) GradlentBrush (абстрактный) LlnearGradlentBrush RadlalGradlentBrush SolldColorBrush TIleBrush (абстрактный) DrawlngBrush ImageBrush VIsualBrush Свойству Background объекта Window задается экземпляр одного из конкрет- ных (не абстрактных) классов, производных от Brush. Все классы Brush входят в пространство имен System.Windows.Media. В этой главе мы обсудим SolidColorBrush и два класса, производных от GradientBrush. Простейшей разновидностью кистей является SolidColorBrush — однородная одноцветная кисть. В одной из программ главы 1 мы задавали цвет клиентской области, включая в программу директиву using System.Windows.Media, а в конст- руктор класса окна — следующий фрагмент: Color clr = Color.FromRgb(0. 255. 255): SolldColorBrush brush = new SolldColorBrush(clr); Background = brush; В результате фоновая область окрашивалась в бирюзовый цвет. Конечно, все можно сделать и в одной строке: Background = new Sol 1dColorBrush(Col or.FromRgb(0, 255, 255)); Класс SolidColorBrush также содержит непараметризованный конструктор и свой- ство Color, позволяющее изменить цвет кисти после создания объекта: SolldColorBrush brush = new Sol1dColorBrush(); brush.Color = Color.FromRgb(128, 0. 128): Следующая программа изменяет цвет фона клиентской области в зависи- мости от приближения указателя мыши к центру окна. Как и большинство будущих программ в этой книге, она содержит директиву using для пространства имен System.Windows.Media.
Базовые кисти 37 VaryTheBackground.cs // VaryTheBackground.cs (с) 2006 by Charles Petzold И using System; using System.Windows: using System.Windows.Input; using System.Windows.Media; namespace Petzold.VaryTheBackground ( public class VaryTheBackground : Window { SolidColorBrush brush = new SolidColorBrush(Colors.Black); [STAThread] public static void Main!) Application app = new Application!); app.Run(new VaryTheBackground!)); } public VaryTheBackgroundO { Title = "Vary the Background"; Width = 384; Height = 384; Background = brush: } protected override void OnMouseMove(MouseEventArgs args) { double width = Actual Width - 2 * SystemParameters.ResizeFrameVerticalBorderWidth: double height = Actual Height - 2 * SystemParameters.ResizeFrameHorlzontalBorderHeight - SystemParameters.CaptionHeight; Point ptMouse = args.GetPosition(this): Point ptCenter = new Point(width / 2, height / 2): Vector vectMouse = ptMouse - ptCenter; double angle = Math.Atan2(vectMouse.Y, vectMouse.X); Vector vectEllipse = new Vector(width / 2 * Math.Cos(angle), height / 2 * Math.Sin(angle)): Byte byLevel = (byte) (255 * (1 - Math.Mind. vectMouse.Length / vectEllipse.Length))): Color clr = brush.Color: clr.R = clr.G = clr.В = byLevel: brush.Color = clr: } } }
38 Глава 2. Базовые кисти При перемещении указателя мыши к центру клиентской области фон посте- пенно светлеет. Если указатель выходит за пределы воображаемого эллипса, за- полняющего клиентскую область, фон становится черным. Все содержательные действия выполняются переопределенным методом Оп- MouseMove, вызываемым при каждом перемещении мыши в клиентской облас- ти программы. Сначала метод должен вычислить размер клиентской области; для этого он берет свойства ActualWidth и ActualHeight окна, а затем вычитает из них размеры рамки и заголовка, полученные из статических свойств класса SystemParameters. Координаты указателя мыши получаются вызовом метода GetPosition класса MouseEventArgs, а полученный объект Point сохраняется в ptMouse. Полученная точка находится на некотором расстоянии от центра клиентской области — структуры Point с именем ptCenter. Затем метод вычитает ptCenter из ptMouse. В документации по структуре Point сказано, что результат вычитания между двумя объектами Point представляет собой объект типа Vector, сохраняемый этим методом под именем vectMouse. С математической точки зрения вектор определя- ется длиной и направлением. Длина vectMouse соответствует расстоянию между ptCenter и ptMouse, а для ее получения используется свойство Length структуры Vector. Направление вектора определяется свойствами X и Y, представляющими направление от начала координат (точки (0,0)) в точку (X,Y). В нашем конкрет- ном случае значение vectMouse.X равно разности ptMouse.X и ptCenter.X (аналогич- но для Y). Направление объекта Vector также может быть представлено в виде угла. Структура Vector содержит статический метод с именем AngleBetween, вычисляю- щий угол между двумя объектами Vector. Метод OnMouseMove в программе Vary- TheBackground демонстрирует прямое вычисление угла vectMouse через арктан- генс отношения свойств Y и X. Угол измеряется в радианах по часовой стрелке от горизонтальной оси. Затем полученный угол используется для вычисления другого объекта Vector, представляющего расстояние от центра клиентской облас- ти до точки эллипса, заполняющего клиентскую область. Интенсивность серого пропорциональна отношению длин двух векторов. Метод OnMouseMove получает объект Color, связанный с SolidColorBrush, задает все три цветовые составляющие для формирования оттенка серого, а затем зада- ет новое значение свойству Color кисти. То, что программа работает, на первый взгляд выглядит удивительно. Разу- меется, кто-то перерисовывает клиентскую область при каждом изменении кис- ти, но все это происходит автоматически. Динамическое поведение программы возможно благодаря тому, что класс Brush является производным от класса Freezable, реализующего событие с именем Changed. Событие инициируется при каждом изменении объекта Brush; именно благодаря ему любое изменение кисти приводит к перерисовке фона. Событие Changed и аналогичные механизмы широко используются в реали- зации анимации и других возможностей WPF. Подобно тому, как класс Colors содержит коллекцию из 141 статического свойства, доступных только для чте- ния, класс с именем Brushes (снова обратите внимание на множественное число)
Базовые кисти 39 предоставляет 141 статическое свойство, доступное только для чтения. Имена этих свойств совпадают с именами из коллекции Colors, но возвращают они объ- екты типа SolidColorBrush. Для задания свойства Background вместо конструкции Background = new SolidColorBrush(Colors.PaleGoldenrod): может использоваться упрощенная запись Background = Brushes.Pal eGoldenrod; Конечно, эти две команды окрашивают окно в одинаковые цвета. И все же между двумя решениями существует различие, которое проявляется в програм- мах вроде VaryTheBackground. Попробуйте заменить определение поля SolidColorBrush brush = new Sol 1dColorBrush(Colors.Bl ack): на SolldColorBrush brush = Brushes.Bl ack; Перекомпилируйте и перезапустите программу. Вы получите исключение недопустимой операции, описание которого гласит; «Не удается задать свойство объекта ‘#FF000000’, доступное только для чтения». Проблема кроется в послед- ней команде метода OnMouseMove, которая пытается задать свойство Color кисти. (Шестнадцатеричное число в апострофах — текущее значение свойства Color.) Объекты SolidColorBrush, возвращаемые классом Brushes, находятся в зафикси- рованном (frozen) состоянии; это значит, что их дальнейшие изменения невоз- можны. Фиксация, как и событие Changed, реализована в классе Freezable, базовом по отношению к Brush. Если свойство CanFreeze объекта Freezable истинно, зна- чит, его можно зафиксировать вызовом Freeze. Признаком фиксации является истинное свойство IsFrozen. Фиксация объектов повышает быстродействие, по- скольку эти объекты не нужно отслеживать на предмет изменений. Зафиксиро- ванный объект Freezable может совместно использоваться разными программными потоками, тогда как для незафиксированного объекта Freezable это невозможно. Снять фиксацию с зафиксированного объекта невозможно, но можно создать не- зафиксированную копию. Следующее определение поля в VaryTheBackground будет работать нормально: SolidColorBrush brush = Brushes.Black.Clone(); Хотите посмотреть, как выглядит перерисовка клиентской области окна раз- ными стандартными цветами? В программе FlipThroughTheBrushes клавиши Т и Ф используются для перебора кистей. FlipThroughTheBrushes.cs //----------------------------------------------------- // FlipThroughTheBrushes.cs (с) 2006 by Charles Petzold //----------------------------------------------------- using System; using System.Reflection; using System.Windows;
40 Глава 2. Базовые кисти using System.Windows.Input; using System.Windows.Media: namespace Petzold.Fl 1pThroughTheBrushes { public class FlipThroughTheBrushes : Window { Int Index = 0; Propertylnfof] props: [STAThread] public static void MalnO { Application app = new Application): app.Run(new Fl 1pThroughTheBrushes()); public Fl 1pThroughTheBrushes() { props = typeof(Brushes).GetPropert1es(B1nd1ngFlags.Pub!1c | BlndlngFlags.Static): SetT1tleAndBackground(): protected override void 0nKeyDown(KeyEventArgs args) { If (args.Key == Key.Down || args.Key == Key.Up) { Index += args.Key -= Key.Up ? 1 : props.Length - 1: Index %= props.Length: SetT1tleAndBackground(): } base.OnKeyDown(args): } void SetTitleAndBackgroundO { Title = "Flip Through the Brushes - " + props[1ndex].Name; Background = (Brush) props[1ndex].GetValue(null, null); } Для получения членов класса Brushes в программе используется механизм рефлексии. В первой строке конструктора выражение typeof(Brushes) создает объ- ект типа Туре. Класс Туре определяет метод GetProperties, возвращающий массив объектов Propertyinfo; каждый объект массива соответствует одному из свойств класса. Обратите внимание: программа специально ограничивается открытыми и статическими свойствами класса Brushes, используя аргумент BindingFlags при вызове GetProperties. В данном случае без этого ограничения можно обойтись, потому что все свойства Brushes являются открытыми и статическими, ио вреда от него тоже не будет.
Базовые кисти 41 Как в конструкторе, так и в переопределении OnKeyDown вызывается ме- тод SetTitleAndBackground, задающий свойству Background один из членов класса Brushes. Выражение props[0].Name возвращает строку с именем первого свойства класса; это строка AliceBlue. Выражение props[0].GetValue(null, null) возвращает объект SolidColorBrush. В нашем случае методу GetValue должны передаваться два аргумента null. Обычно в первом аргументе передается объект, для которого определяется значение свойства. Поскольку свойство Brushes является статическим, такого объекта не существует. Второй аргумент используется только для свойств-ин- дексаторов. Пространство имен System.Windows содержит класс SystemColors. Как и Colors с Brushes, этот класс содержит исключительно статические свойства, доступные только для чтения, возвращающие структуры Color и объекты SolidBrushColor. Класс возвращает информацию о настройках цветов текущего пользователя из реестра Windows. Например, SystemColors.WindowColor определяет выбранный пользователем цвет фона клиентской области, a SystemColors.WindowTextColor — выбранный пользователем цвет клиентской области. SystemColors.WindowBrush и SystemColors.WindowTextBrush возвращают объекты SolidColorBrush, созданные с теми же цветами. В большинстве реальных приложений для вывода простого текста и монохромной графики следует использовать именно эти цвета. Объекты кистей, возвращаемые из SystemColors, зафиксированы. Программа сможет изменить кисть Brush brush = new SystemColorBrush(SystemColors.WindowColor): но не кисть Brush brush = SystemColors.WindowBrush: Фиксация возможна только для объектов, производных от Freezable. Зафик- сированный объект Color в принципе невозможен, потому что Color является структурой. Одну из альтернатив для однородных кистей составляют градиентные кисти с постепенным переходом двух и более цветов. Обычно применение градиент- ных кистей считается делом нетривиальным, но в WPF такие кисти создаются легко, а в современных цветовых схемах они встречаются достаточно часто. В простейшей форме для создания линейной градиентной кисти LinearGradient- Brush требуются два объекта Color (назовем их clrl и clr2), а также два объекта Point (ptl и pt2). Точка ptl окрашивается в цвет clrl, а точка pt2 окрашивается в цвет clr2. Линия, соединяющая ptl с pt2, состоит из переходных цветов, так что ее се- редина окрашена в средний цвет между clrl и с1г2. Все линии, перпендикуляр- ные [ptl, pt2], окрашиваются равномерно в цвет, образованный сочетанием двух основных цветов. О том, что происходит по другую сторону ptl и pt2, мы пого- ворим чуть позднее. А теперь хорошая новость: обычно для применения градиентной кисти зада- ются две точки в пикселах или (в случае WPF) аппаратно-независимых едини- цах, а если градиентная закраска применяется к фону окна — при изменении размеров окна точки положение точек приходится пересчитывать заново.
42 Глава 2. Базовые кисти Градиентная кисть WPF обладает особенностями, благодаря которым кисть не нужно создавать заново или изменять в зависимости от размеров окна. По умол- чанию две точки задаются относительно поверхности, окрашиваемой градиент- ной кистью, причем размеры поверхности составляют 1х1единицу. В левом верх- нем углу поверхности находится точка (0,0), а в правом нижнем — точка (1,1). Предположим, вы хотите, чтобы левый верхний угол клиентской области был окрашен в красный цвет, а правый нижний — в синий и эти углы были соедине- ны градиентной заливкой. Используйте следующий конструктор с заданием двух цветов и двух точек: Li nearGradientBrush brush = new LinearGradientBrush(Colors.Red. Colors.Blue. new Point(0, 0). new Pointd. D); Именно это происходит в следующей программе. GradiateTheBrush.cs //------------------------------------------------- // GradiateTheBrush.cs (с) 2006 by Charles Petzold //------------------------------------------------- using System: using System.Windows: using System.Windows.Input: using System.Windows.Media: namespace Petzold.Gradi ateTheBrush { public class GradiateTheBrush : Window { [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new GradiateTheBrush()); } public GradiateTheBrush() { Title = "Gradiate the Brush"; LinearGradientBrush brush = new LinearGradientBrush(Colors.Red. Colors.Blue. new Point(0, 0). new Pointd, D): Background = brush; } } } Изменение размера клиентской области приводит к автоматическому измене- нию градиентной кисти. Как и прежде, это возможно благодаря событию Change, реализуемому классом Freezable. Задание точек в относительной системе координат часто бывает удобным, но это не единственный вариант. Класс GradientBrush определяет свойство MappingMode,
Базовые кисти 43 которому присваиваются значения из перечисления BrushMappingMode. Допусти- мых значении всего два: RelativeToBoundingBox (относительно ограничивающего прямоугольника, используется по умолчанию) и Absolute (аппаратно-независимые единицы). В шестнадцатеричной RGB-записи цвет левого верхнего угла клиент- ской области в программе GradiateTheBrush представлен тройкой FF-00-00, а цвет правого нижнего угла — тройкой 00-00-FF. Точка в середине между ними имеет цвет 7F-00-7F или 80-00-80 (в зависимости от округления), потому что по умол- чанию используется режим линейной интерполяции (свойство Colorinterpolation- Mode равно ColorlnterpolationMode.SRgbLinearlnterpolation). При другом возможном значении — ColorlnterpolationMode.ScRgbLinearlnterpolation — промежуточная точ- ка определяется значением scRGB 0,5-0-0,5, что соответствует значению sRGB BC-00-BC. Для создания простых горизонтальных и вертикальных градиентов проще использовать следующий конструктор LinearGradientBrush: new LinearGradientBrush(clrl, clr2. angle); Угол (angle) задается в градусах. Значение 0 определяет горизонтальный гра- диент с точкой clrl слева; такой вызов эквивалентен new LinearGradientBrush(clrl, clr2. new Point(0, 0). new Pointd, 0)): Значение 90 определяет вертикальный градиент с точкой clrl наверху; такой вызов эквивалентен new LinearGradientBrush(clrl. clr2, new Po1nt(0. 0). new Po1nt(0, D); С другими углами дело обстоит несколько сложнее. В общем случае первая точка всегда определяет начало координат, а вторая вычисляется по формуле new Point(cos(angle), sin(angle)) Например, для угла 45° вторая точка имеет приблизительные координаты (0,707, 0,707). Помните, что координаты пропорциональны размерам клиентской области, и если последняя не является квадратной (как это обычно бывает), линия между двумя точками пройдет не под углом 45°. Кроме того, довольно большая часть правого нижнего угла находится за вычисленной точкой. Что произойдет в ней? По умолчанию она будет закрашена вторым цветом. Данный аспект поведения определяется свойством SpreadMethod, принимающим значения из перечисления GradientSpreadMethod. По умолчанию используется значение Pad, при котором для заполнения внешних областей используется конечный цвет градиента. Другие допустимые значения — Reflect и Repeat. Попробуйте вклю- чить следующий фрагмент в GradiateTheBrush: LinearGradientBrush brush = new LinearGradientBrush(Colors.Red, Colors.Blue, new Point(0, 0), new Point(0.25, 0.25)): brush.SpreadMethod = GradientSpreadMethod.Reflect; Кисть рисует градиент от красного к синему цвету между точками (0, 0) и (0,25, 0,25), затем от синего к красному между точками (0,25, 0,25) и (0,5, 0,5),
44 Глава 2. Базовые кисти затем от красного к синему между точками (0,5, 0,5) и (0,75, 0,75) и, наконец, от синего к красному между точками (0,75, 0,75) и (1, 1). Если сделать окно очень узким или коротким, чтобы различия по ширине и высоте были особенно контрастными, линии постоянного цвета становятся практически вертикальными или горизонтальными. Возможно, вы предпочитае- те, чтобы в градиенте между двумя противоположными углами линия постоян- ного цвета проходила между двумя другими углами. Возможно, сказанное луч- ше пояснить рисунком. Вот как выглядит клиентская область GradientTheBrush при растяжении по горизонтали. Синий Пунктиром обозначены линии постоянного цвета, которые всегда перпенди- кулярны линии, соединяющей ptl с pt2. Допустим, вы предпочитаете градиент следующего вида. На этот раз линия, окрашенная в малиновый цвет, проходит между двумя уг- лами. Проблема в том, чтобы вычислить положение точек ptl и pt2, при котором соединяющая эти точки линия была бы перпендикулярна линии, соединяющей нижний левый угол с правым верхним.
Базовые кисти 45 Длина отрезков от центра прямоугольника до точек ptl и pt2 (назовем ее L) вычисляется по формуле L_ WH J\V2 + Н2 ’ где 1У — ширина окна, а Н — его высота. Вот как выглядит та же клиентская об- ласть с дополнительными линиями и метками. Обратите внимание: отрезок L параллелен линии, соединяющей ptl с pt2. Си- нус угла а можно вычислить двумя способами. Во-первых, он равен высоте Н, деленной на длину диагонали прямоугольника: sm(ot) = ,______ —. Jw2 + н2 Во-вторых, можно принять L за противолежащий катет, a W за гипотенузу: . / ч L sm(a) = —. W Объединив эти два уравнения, получаем формулу для L. Следующая программа создает кисть LinearGradientBrush с абсолютным режимом отображения (MappingMode = Absolute), но не для ее непосредственного использо- вания. Конструктор также устанавливает обработчик события SizeChanged, сра- батывающий при каждом изменении размеров окна. AdjustTheGradient.cs //------------------------------------------------- // AdjustTheGrad1ent.cs (с) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Windows; using System.Windows.Input; using System.Windows.Media; namespace Petzold.Adjust!heGradlent
46 Глава 2. Базовые кисти { class AdjustTheGrad1ent: Window LInearGradlentBrush brush; [STAThread] public static void MalnO { Application app = new ApplicationO; app.Run(new AdjustTheGradienU)); ) public AdjustTheGradienU) { Title = "Adjust the Gradient": SizeChanged += WindowOnSizeChanged; brush = new LinearGradientBrush(Colors.Red. Colors.Blue. 0): brush.MappingMode = BrushMappingMode.Absolute: Background = brush: } void WindowOnSizeChanged(object sender. SizeChangedEventArgs args) { double width = Actual Width - 2 * SystemParameters.ResIzeFrameVertlcalBorderWidth; double height = ActualHelght - 2 * SystemParameters.ResIzeFrameHorlzontalBorderHelght - SystemParameters.CaptlonHelght; Point ptCenter = new Po1nt(w1dth / 2, height / 2); Vector vectDIag = new Vector(w1dth. -height); Vector vectPerp = new Vector(vectDIag.Y, -vectDIag.X); vectPerp.Normal 1ze(): vectPerp *= width * height / vectDIag.Length; brush.StartPoInt = ptCenter + vectPerp; brush.EndPoInt = ptCenter - vectPerp; } ) } Обработчик начинается с вычисления ширины и высоты клиентской облас- ти, как в приведенной ранее программе VaryTheBackground. Объект vectDiag класса Vector представляет диагональ от левого нижнего к правому верхнему углу. Его также можно вычислить вычитанием координат левого нижнего угла из правого верхнего: vectDiag = new Po1nt(w1dth, 0) - new Po1nt(0, height); Объект vectPerp перпендикулярен диагонали. Перпендикулярные векторы легко создаются перестановкой свойств X и Y с изменением знака одного из них.
Базовые кисти 47 Метод Normalize делит X и Y па свойство Length, чтобы значение Length стало равно 1. Затем обработчик умножает vectPerp на длину, которая ранее обозна- чалась как L. На последнем шаге задаются свойства StartPoint и EndPoint объекта Linear- GradientBrush. Эти свойства обычно задаются в одном из конструкторов кисти, и только они определяются в самом классе LinearGradientBrush (некоторые свой- ства также наследуются от абстрактного класса GradientBrush). И снова напомню, что программе достаточно изменить свойство LinearGradient- Brush, чтобы окно было автоматически перерисовано обновленной кистью. «Вол- шебство» работает благодаря событию Changed, определяемому классом Freezable (и другим связанным возможностям WPF). Возможности класса LinearGradientBrush не ограничиваются двумя рассмот- ренными программами — при помощи кисти также можно создать градиентную заливку с несколькими цветами. Для этого необходимо воспользоваться свойст- вом Gradientstops, определяемым классом GradientBrush. Свойство Gradientstops является объектом типа GradientStopCollection — кол- лекции объектов Gradientstop. Gradientstop определяет два свойства Color и Offset, и конструктор с этими свойствами: new GradientStop(clr, offset) Значение свойства Offset обычно лежит в интервале от 0 до 1 и представ- ляет относительное расстояние между StartPoint и EndPoint. Например, для точек StartPoint (70, 50) и EndPoint (150, 90) свойство Offset, равное 0,25, определяет точку на четверти расстояния от StartPoint до EndPoint, или (90, 60). Конечно, если точка StartPoint имеет координаты (0, 0), a EndPoint — координаты (0, 1), (1, 0) или (1, 1), координаты точки, соответствующей Offset, определяется гораз- до проще. Следующая программа создает горизонтальную кисть LinearGradientBrush с се- мью объектами Gradientstop для семи цветов радуги. Каждая точка Gradientstop смещается вправо на 1/6 ширины окна по отношению к предыдущей. FollowTheRainbow.cs //-------------------------------------------------- // FollowTheRainbow.cs (с) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Windows; using System.Windows.Input; using System.Windows.Media; namespace Petzold.Fol 1owTheRai nbow { class FoilowTheRainbow: Window { [STAThread] public static void MainO
48 Глава 2. Базовые кисти Application app = new ApplicationO; app.Run(new FollowTheRainbowO); ) public FollowTheRainbowO { Title = "Follow the Rainbow"; LinearGradientBrush brush = new LinearGradientBrush(); brush.StartPoint = new Point(0, 0): brush.EndPoint = new Pointd, 0); Background = brush; brush.GradientStops.Add(new GradientStop(Colors.Red, 0)); brush.Gradi entStops.Add(new Gradi entStop(Colors.Orange, . 17)); brush.GradientStops.Add(new GradientStop(Colors.Yel1ow, .33)); brush.GradientStops.Add(new GradientStop(Colors.Green. .5)); brush.GradientStops.Add(new GradientStop(Colors.Blue, .67)); brush.GradientStops.Add(new GradientStop(Colors.Indigo, .84)); brush.Gradientstops.Add(new GradientStop(Colors.Violet, 1)); } } ) Переключиться с линейной градиентной кисти (LinearGradientBrush) на ради- альную (RadialGradientBrush) совсем несложно. Все, что для этого потребуется, — изменить имя класса кисти, а также удалить команды присваивания StartPoint и EndPoint; CircleTheRainbow.cs //-------------------------------------------------- // CircleTheRainbow.cs (с) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Windows; using System.Windows.Input; using System.Windows.Media; namespace Petzold.Ci rcleTheRai nbow { public class CircleTheRainbow : Window { [STAThread] public static void MainO Application app = new ApplicationO; app.Run(new CircleTheRainbowO); } public CircleTheRainbowO
Базовые кисти 49 { Title = "Circle the Rainbow"; RadlalGradlentBrush brush = new Rad1alGrad1entBrush(); Background = brush; brush.Gradientstops.Add(new Grad1entStop(Colors.Red. 0)): brush.GradlentStops.Add(new GradlentStop(Colors.Orange, . 17)); brush.Gradientstops.Add(new GradlentStop(Colors.Yellow, .33)); brush.GradlentStops.Add(new GradlentStop(Colors.Green, .5)); brush.GradlentStops.Add(new GradlentStop(Col ors.Bl ue, .67)); brush.Gradientstops.Add(new Grad1entStop(Colors.Indigo, .84)); brush.Gradientstops.Add(new Grad1entStop(Colors.V1olet, 1)); ) } } Теперь заливка начинается с красного цвета в центре клиентской области и распространяется наружу до фиолетового по границам эллипса, заполняюще- го клиентскую область. Углы клиентской области за пределами эллипса закра- шиваются фиолетовым (по умолчанию SpreadMethod=Fill). Конечно, в классе RadialGradientBrush определяется ряд полезных свойств со значениями по умолчанию. Три из них определяют эллипс. Свойство Center пред- ставляет объект Point со значением по умолчанию (0,5, 0,5), соответствующим центру закрашиваемой области. Вещественные свойства RadiusX и RadiusY задают полуоси эллипса. Значения по умолчанию равны 0,5, так что эллипс касается сторон закрашиваемого прямоугольника. Периметр эллипса, определяемого свойствами Center, RadiusX и RadiusY, ок- рашивается в цвет со свойством Offset=l. (Фиолетовый в примере CircleThe- Rainbow.) Четвертое свойство называется Gradientorigin; как и свойство Center, оно представляет собой объект Point со значением по умолчанию (0,5, 0,5). Как подсказывает название, точка Gradientorigin является исходной при построе- нии градиента. Именно в ней выводится цвет с Offset=0 (красный в примере CircleTheRainbow). Градиент строится между Gradientorigin и границей эллипса. Если Gradientorigin совпадает с Center (по умолчанию), то градиент строится от центра эллипса к пе- риметру. Если точка Gradientorigin смещена по отношению к Center, градиент будет более плотным там, где Gradientorigin располагается ближе к периметру эллип- са, и более разреженным в отдалении. Чтобы понаблюдать за этим эффектом, вставьте в CircleTheRainbow следующую команду: brush.Gradientorigin = new Po1nt(0.75, 0.75): Попробуйте поэкспериментировать с разными соотношениями свойств Center и Gradientorigin; программа ClickTheGradientCenter предоставляет такую возмож- ность. В ней используется конструктор RadialGradientBrush с двумя аргумента- ми, определяющий цвет Gradientorigin и цвет на периметре эллипса. При этом
50 Глава 2. Базовые кисти программа задает свойства RadiusX п RadiusY равными 0,10, а свойство Spread- Method равным Repeat, чтобы кисть выглядела как набор концентрических гра- диентных кругов. ClickTheGradientCenter.cs //-------------------------------------------------------- // С11ckTheGrad1entCenter.es (с) 2006 by Charles Petzold И--------------------------------------------------------- using System: using System.Windows; using System.Hindows.Input; using System.Windows.Media; namespace Petzold. Cl 1ckTheGradlentCenter class ClickTheRadlentCenter : Window { RadlalGradlentBrush brush: CSTAThread] public static void MalnO Application app = new ApplicationO; app.Run(new Cl 1ckTheRadlentCenter()); } public Cl 1ckTheRadlentCenterО { Title = "Click the Gradient Center"; brush = new Rad1alGradientBrush(Colors.Wh1te, Colors.Red); brush.RadiusX = brush.RadiusY = 0.10; brush.SpreadMethod = GradientSpreadMethod.Repeat; Background = brush; } protected override void OnMouseDownCMouseButtonEventArgs args) { double width = Actualwidth - 2 * SystemParameters.ResIzeFrameVerticalBorderWIdth; double height = ActualHelght - 2 * SystemParameters.ResIzeFrameHorlzontalBorderHelght - SystemPa rameters.Capt1onHeight; Point ptMouse = args.GetPosltlon(thls); ptMouse.X /= width; ptMouse.Y /= height; If (args.ChangedButton == MouseButton.Left) {
51 Базовые кисти brush.Center = ptMouse; brush.Gradientorigin = ptMouse; } else if (args.ChangedButton == MouseButton.Right) brush.Gradientorigin = ptMouse; } } } Программа переопределяет OnMouseDown для обработки щелчков в клиент- ской области. Левая кнопка мыши задает свойствам Center и Gradientorigin одина- ковые значения, а вся кисть просто сдвигается от центра клиентской области. Щелчок правой кнопкой мыши изменяет только Gradientorigin. Проследите за тем, как градиент сжимается с одной стороны и растягивается с другой. Эффект получается настолько интересным, что я решил анимировать его. Следующая программа RotateTheGradientOrigin не использует средства анима- ции, встроенные в WPF. Вместо этого свойство Gradientorigin просто изменяется по таймеру. В .NET существуют минимум четыре класса таймеров, причем трем из них присвоено имя Timer. Классы Timer в пространствах System.Threading и System. Timers не могут использоваться в этой конкретной программе, потому что собы- тия таймера происходят в другом программном потоке, а объекты Freezable долж- ны изменяться в том потоке, в котором они были созданы. Класс Timer из про- странства System.Windows.Forms инкапсулирует стандартный таймер Windows, но для работы с ним придется включить дополнительную ссылку на сборку System. Windows.Forms.dll. Чтобы класс таймера мог использоваться в WPF-программе, а события проис- ходили в программном потоке приложения, следует использовать класс Dispatcher- Timer в пространстве имен System.Windows.Threading. Свойство Interval определяет периодичность срабатывания таймера и не может быть меньше 10 миллисекунд. Программа создает небольшое квадратное окно со стороной 4 дюйма. Это сделано для того, чтобы программа не отнимала слишком много ресурсов про- цессора. RotateTheGradientOrigin.cs //-------------------------------------------------------- // RotateTheGradientOrigin.es (с) 2006 by Charles Petzold //-------------------------------------------------------- using System; using System.Windows; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; namespace Petzold.RotateTheGradientOr1gin {
52 Глава 2. Базовые кисти public class RotateTheGradientOrigin : Window { RadialGradientBrush brush: double angle: [STAThread] public static void MainO { Application app = new Applications app.Run(new RotateTheGradientOrigin()): } public RotateTheGradientOriginO { Title = "Rotate the Gradient Origin": WindowStartupLocation = WindowStartupLocation.CenterScreen; Width = 384; // To есть 4 дюйма Height = 384: brush = new RadialGradientBrush(Colors.White, Colors.Blue): brush.Center = brush.Gradientorigin = new Point(0.5, 0.5); brush.RadiusX = brush.RadiusY = 0.10: brush.SpreadMethod = GradientSpreadMethod.Repeat; Background = brush: DispatcherTimer tmr = new DispatcherTimerO: tmr.Interval = TimeSpan.FromMilliseconds(lOO): tmr.Tick += TimerOnTick; tmr.Start(); } void TimerOnTick(object sender. EventArgs args) { Point pt = new Point(0.5 + 0.05 * Math.Cos(angle). 0.5 + 0.05 * Math.Sin(angle)); brush.Gradientorigin = pt: angle += Math.PI / 6: // To есть 30 градусов } } } В этой главе наше внимание было сосредоточено на свойстве Background, но у класса Window есть еще три свойства типа Brush. Одно из них — OpacityMask — наследуется Window от UIEIement. Два других свойства Brush наследуются Window от Control. Первое из них, BorderBrush, рисует рамку по периметру клиентской об- ласти. Попробуйте вставить следующий фрагмент в последнюю программу этой главы: BorderBrush = Brushes.SaddleBrown: BorderThickness = new Thickness(25, 50, 75. 100);
Базовые кисти 53 Структура Thickness обладает четырьмя свойствами Left, Top, Right и Bottom; конструктор с четырьмя аргументами задает эти свойства в указанном поряд- ке. Значения в аппаратно-независимых единицах обозначают толщину рамки на четырех сторонах клиентской области. Если толщина рамки на всех четырех сторонах должна быть одинаковой, воспользуйтесь конструктором с одним ар- гументом: BorderThickness = new Thlckness(50); Разумеется, рамка может закрашиваться градиентной кистью: BorderBrush = new GradientBrush(Colors.Red, Colors.Blue, new Point(0, 0), new Pointd, D): Результат напоминает заполнение клиентской области градиентной кистью (в левом верхнем углу находится красный цвет, в правом нижнем синий), но за- краска производится только по периметру клиентской области.
Концепция ьЭ содержимого Класс Window содержит более 100 открытых (public) свойств, и некоторые из них (например, свойство Title, определяющее заголовок окна) весьма важны. И все же самым важным свойством Window, несомненно, является свойство Content. Его значением является объект, который должен отображаться в клиентской об- ласти окна. Свойству Content можно присвоить строку или растровое изображе- ние, кнопку, полосу прокрутки или любой из 50 с лишним элементов, поддер- живаемых WPF. Короче, свойству Content можно присвоить все, что угодно. Возникает единственная проблема: это может быть только один объект. На первых порах это ограничение создает немало трудностей. Конечно, вско- ре вы научитесь задавать свойству Content объект, который может использовать- ся для размещения других объектов. А пока и одного объекта будет вполне дос- таточно. Класс Window наследует свойство Content от Contentcontrol — класса, производ- ного от Control, являющегося непосредственным предком Window. Практически единственной целью класса Contentcontrol является определение свойства Content и нескольких связанных с ним свойств и методов. Свойство Content определяется с типом object; такое определение предполага- ет, что свойству может быть задан любой объект, и это почти правда. Я говорю «почти», потому что свойству Content нельзя задать другой объект типа Window. Программа выдаст runtime-исключение, в котором говорится, что класс Window должен быть «корнем дерева», а не ветвью другого объекта Window. Например, свойству Content можно задать текстовую строку. DisplaySomeText.cs //--------------------------------------------- // DisplaySomeText.cs (с) 2006 by Charles Petzold //--------------------------------------------- using System; using System.Windows: using System.Windows.Input: using System.Windows.Media: namespace Petzol d.Di splaySomeText
Концепция содержимого 55 public class DisplaySomeText : Window t [STAThread] public static void MainO { Application app = new Application); app.Run(new DisplaySomeTextO); } public DisplaySomeTextO Title = "Display Some Text"; Content = "Content can be simple text!"; Программа выводит текстовое сообщение в левом верхнем углу клиентской области. Если уменьшить ширину окна так, чтобы текст в нем не помещался, ав- томатический перенос не выполняется — текст просто усекается (хотя в него можно вставлять разрывы строк — метасимволы \г, \п или \г\п). Включенная в программу директива using System.Windows.Media позволит вам поэкспериментировать со свойствами, определяющими цвет и шрифт текста. Все эти свойства определяются классом Control, от которого наследует класс Window. Ничто (кроме хорошего вкуса) не помешает вам задать шрифт для отображе- ния текста в клиентской области следующим образом: FontFamily = new FontFamily("Comiс Sans MS"); FontSize = 48; Этот фрагмент может находиться в любом месте конструктора класса. В WPF не существует класса шрифта Font. Первая строка кода создает ссылку на объект FontFamily — семейство связанных гарнитур. В системе Windows семей- ства шрифтов обладают знакомыми именами Courier New, Times New Roman, Arial и, конечно, Comic Sans MS. Гарнитурой называется одна из возможных разновидностей семейства — на- пример, Times New Roman Bold, Times New Roman Italic или Times New Roman Bold Italic. He все варианты возможны в каждом семействе, а в некоторых се- мействах определены разновидности, влияющие на ширину отдельных симво- лов — скажем, Arial Narrow. Термин «шрифт» (font) обычно обозначает комбинацию определенной гарни- туры с определенным размером. Стандартной единицей измерения размера шриф- та является пункт. В традиционной полиграфии пункт равен 0,01374 дюйма, но в компьютерной полиграфии он считается равным 1/72 дюйма. Таким образом, высота символов шрифта с размером 36 пунктов составляет около 1/2 дюйма. В WPF размер шрифта задается свойством FontSize, но его значение измеря- ется не в пунктах. Как и все остальные метрики WPF, размер шрифта задается в аппаратно-зависимых единицах 1/96 дюйма. Например, свойство FontSize=48
56 Глава 3. Концепция содержимого эквивалентно 36 пунктам. Если вы привыкли задавать размеры шрифтов в пунк- тах, просто умножьте их на 4/3 при задании свойства FontSize. По умолчанию свойство FontSize равно 11, что соответствует 8,25 пункта. Пол- ное имя гарнитуры указывается в конструкторе FontFamily: FontFamily = new FontFamily("Times New Roman Bold Italic") ; FontSize = 32; Так создается шрифт Times New Roman Bold Italic с размером 24 пункта. Впрочем, на практике в конструкторе чаще указывается название семейства, а полужирное и курсивное начертание определяется установкой свойств Fontstyle и Fontweight: FontFamily = new FontFamilyCTimes New Roman"): FontSize = 32: Fontstyle = Fontstyles.Italic; Fontweight = Fontweights.Bold: Обратите внимание: свойствам Fontstyle и FontWeight задаются статические, доступные только для чтения свойства классов Fontstyles (множ.) и FontWeights (множ.). Интересная разновидность: Fontstyle = Fontstyles.Oblique: Курсивное начертание часто стилистически отличается от прямого (различия видны на примере строчной буквы «а»), а при наклонном (oblique) начертании все символы прямой гарнитуры наклоняются вправо. Для некоторых семейств шрифтов свойству FontStretch задается статическое свойство класса FontStretches. Читатель уже знаком со свойством Background, определяющим закраску фона клиентской области. Цвет выводимого текста определяется свойством Foreground. Попробуйте выполнить следующий фрагмент: Brush brush = new LinearGradientBrush(Colors.Black, Colors.White. new Point(0, 0). new Pointd, 1)); Background = brush; Foreground = brush; И текст, и фон теперь закрашиваются одной кистью; казалось бы, выводи- мый цвет должен стать невидимым. Впрочем, на практике этого не происходит. Как упоминалось в главе 2, градиентные кисти, используемые для закраски фона, по умолчанию автоматически подстраиваются под размеры клиентской области. Кисть для закраски выводимого текста тоже автоматически подстраивается под размер содержимого — текстовой строки. Изменение размера окна на нее не влияет. Впрочем, если подогнать размеры клиентской области под размеры тек- ста, две кисти сольются, и текст исчезнет. Теперь попробуйте следующий вариант: SizeToContent = SizeToContent.WidthAndHeight; Свойство SizeToContent, определяемое классом Window, заставляет окно авто- матически подстраиваться под размеры содержимого. Если для закраски текста
Кон цепция содержимого 57 и фона используется та же кисть LinearGradientBrush, текст становится невидимым. Свойству SizeToContent присваиваются элементы перечисления SizeToContent: Manual (используется по умолчанию), Width, Height или WidthAndHeight. В трех послед- них случаях окно подстраивает под размер содержимого свою ширину, высоту или ширину с высотой. Данное свойство чрезвычайно удобно, и оно часто ис- пользуется при проектировании диалоговых окон и других форм. При измене- нии размеров окна по размерам содержимого дальнейшее изменение размеров окна обычно запрещается командами ResizeMode = ResizeMode.CanMininri ze; или ResizeMode = ResizeMode.NoResize; Внутри клиентской области можно создать обрамление способом, показан- ным в конце главы 2: BorderBrush = Brushes.SaddleBrown: BorderThickness = new Thickness(25, 50, 75, 100); Присутствие обрамления учитывается как кистью вывода текста, так и мето- дом SizeToContent. Содержимое всегда выводится внутри обрамления. Вывод текстовой строки в программе DisplaySomeText в действительности имеет гораздо более общий характер, чем кажется на первый взгляд. Как из- вестно, все объекты поддерживают метод ToString, возвращающий текстовое представление объекта. Метод ToString используется окном для вывода объек- та. Чтобы убедиться в этом, достаточно задать свойству Content любой объект вместо строки; Content = Math.PI; или Content = DateTime.Now; В обоих случаях выведенный текст совпадает со строкой, возвращаемой ме- тодом ToString. Если класс объекта не переопределяет ToString, то вызов ToString просто возвращает полное имя класса. Пример; Content = EventArgs.Empty; В окне отображается строка «System.EventArgs». Единственное исключение относится к массивам. Если использовать команду вида Content = new int[57]; в окне выводится текст «Int32[] Array», тогда как метод ToString возвращает стро- ку «System.Int32[]». Следующая программа задает свойству Content пустую тек- стовую строку, после чего добавляет в нее новые символы, вводимые с клавиа- туры. В целом она похожа на программу TypeYourTitle из главы 1, но эта версия также позволяет вводить символы перевода строки и табуляции.
58 Глава 3. Концепция содержимого RecordKeystrokes.cs //-------------------------------------------------- // RecordKeystrokes.cs (с) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Windows: using System.Windows.Input: using System.Windows.Media: namespace Petzold.RecordKeystrokes { public class RecordKeystrokes : Window { [STAThread] public static void MainO { Application app = new Application): app.Run(new RecordKeystrokes()); publi c RecordKeystrokes() { Title = "Record Keystrokes"; Content = ""; } protected override void OnTextInput(TextCompositionEventArgs args) { base.OnText Input(args): string str = Content as string: if (args.Text == "\b") { if (str.Length > 0) str = str.Substring^, str.Length - 1); } else { str += args.Text; } Content = str: } } } Программа RecordKeystrokes работает только потому, что свойство Content изменяется с каждым нажатием клавиши, а класс Window реагирует на измене- ния перерисовкой клиентской области. Стоит внести в программу небольшое изменение, и она работать перестанет. Попробуйте определить пустую строку в виде поля: string str = :
Концепция содержимого 59 Задайте эту переменную свойству Content в конструкторе: Content = str; Удалите из переопределения OnTextlnput эту команду и определение str. Те- перь программа не работает. Обратите внимание на команду присваивания в On- Textlnput: str += args.Text: Она эквивалентна следующей команде: str = str + args.Text; В обеих командах проблема заключается в том, что строковый объект, воз- вращаемый операцией конкатенации, отличен от строкового объекта на входе. Напомню, что строки являются неизменяемыми (immutable). Операция конкате- нации создает новую строку, но в свойстве Content по-прежнему хранится исход- ная строка. Попробуем действовать иначе. Для этого решения в программу необходимо включить директиву System.Text. Определите объект StringBuilder как поле: StringBuilder build = new StringBuiIder("text"); В конструкторе программы задайте этот объект свойству Content: Content = build: Естественно, в окне будет выводиться строка «text», так как метод ToString объекта StringBuilder возвращает построенную им строку. Замените код метода OnTextlnput следующим фрагментом: if (args.Text == "\b") { if (build.Length > 0) build.Remove(build.Length - 1. 1); else build.Aopend(args.Text); } И снова программа не работает. Хотя программа содержи ! только один объ- ект StringBuilder, окно по может узнать об изменении хранящейся в нем строки и не обновляется новым текстом. Я привел эти примеры, потому что объекты WPF иногда обновляются совер- шенно необъяснимым образом. В действительности чудеса имеют реальное объ- яснение в виде некой формы оповещения о событиях. Если вы будете понимать логику этих событий, это поможет вам лучше разобраться в работе среды. Перера- ботанная программа Record Keystrokes с объектом StringBuilder не будет работать даже в том случае, если вставить в конец переопределения OnTextlnput команду Content = build:
60 Глава 3. Концепция содержимого Окно «видит», что Content присваивается уже присвоенный объект и обнов- ление не требуется. Но опробуйте следующие команды: Content = null; Content = build: На этот раз программа работает. Мы уже знаем, что содержимое окна может быть простым текстом. Однако свойство Content вовсе не предназначено для вывода простого неформатирован- ного текста — нет, нет и нет. Свойство Content разрабатывалось для отображе- ния графических данных, в роли которых может быть экземпляр любого класса, производного от UIEIement. Класс UIEIement занимает исключительно важное место в WPF. Именно этот класс реализует обработку ввода с клавиатуры, мыши и стилуса. Класс UIELement также содержит важный метод с именем OnRender, вызываемый для получения графического представления объекта (пример приводится в конце главы). В отношении свойства Content все объекты делятся на две группы: производ- ные от UIEIement и все остальные. Для вывода объектов второй группы исполь- зуется метод ToString, а для первой — метод OnRender. Классы, производные от UIEIement (и созданные на их основе экземпляры объектов), иногда называются интерфейсными элементами. Единственным классом, напрямую производным от UIEIement, является Frame- workElement, а все интерфейсные элементы WPF являются производными от Frame- workElement. Теоретически UIEIement предоставляет необходимую инфраструктуру событий пользовательского интерфейса и отрисовки экрана, способную поддер- живать различные среды программирования. Одна из таких сред, WPF, образу- ется всеми классами, производными от FrameworkElement. Вероятно, на практике вы не будете различать свойства, методы и события, определенные в UIEIement и FrameworkElement. Одним из часто используемых классов, производных от FrameworkElement, является класс Image. Иерархия классов выглядит так: Object Dispatcherobject (абстрактный) Dependencyobject Visual (абстрактный) UIEIement FrameworkElement Image Класс Image позволяет легко включить графику в документ или приложение. Следующая программа загружает растровое изображение с моего веб-сайта и ото- бражает его в окне. ShowMyFace.cs //---------------------------------------- // ShowMyFace.cs (с) 2006 by Charles Petzold
Концепция содержимого 61 using System: using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media: using System.Windows.Media.Imaging; namespace Petzold.ShowMyFace class ShowMyFace : Window { [STAThread] public static void Main!) { Application app = new Application!): app.Run(new ShowMyFace!)): } public ShowMyFace!) { Title = "Show My Face"; Uri uri = new Uri("http://www.charlespetzold.com/PetzoldTattoo.jpg"); Bitmapimage bitmap = new Bitmaplmage(uri): Image img = new ImageO; img.Source = bitmap: Content = img: } } Вывод изображения требует пары подготовительных шагов. Сначала про- грамма создает объект Uri, определяющий местонахождение изображения. Объ- ект передается конструктору Bitmapimage, загружающему изображение в память (поддерживаются многие популярные форматы, в том числе GIF, TIFF, JPEG и PNG). Класс Image отображает изображение в окне (экземпляр этого класса задается свойству Content окна). Класс Image принадлежит пространству имен System.Windows.Controls. Фор- мально Image не является элементом управления (control) и не наследует от класса Control. Однако пространство имен System.Windows.Controls играет настоль- ко важную роль, что я включаю его почти во все приводимые программы. Класс Bitmapimage находится в пространстве имен System.Windows.Media.Imaging; конеч- но, это пространство очень важно при работе с растровыми изображениями. Тем не менее я обычно включаю директиву using для этого пространства только в том случае, если этого требует программа. Графический файл также может загружаться с локального диска. Для этого в конструктор Uri включается либо полное имя файла, либо относительное имя файла с префиксом file://. Пример:
62 Глава 3. Концепция содержимого Uri uri = new Uri( System. 10.Path.Combi ne( Environment. GetEnvironmentVariableCwindir"),"Gone Fishing.bmp")): Метод Environment.GetEnvironmentVariable возвращает значение переменной сре- ды windir, то есть строку вида C:\WINDOWS. Метод Path.Combine объединяет путь с именем графического файла, чтобы ленивому программисту не пришлось само- му возиться с вставкой разделителей. Либо укажите перед классом Path префикс System.10 (как сделано в примере), либо включите директиву using для этого про- странства имен. Вместо передачи объекта Uri конструктору Bitmapimage, также можно задать объект Uri свойству UriSource класса Bitmapimage. Тем не менее задание этого свой- ства рекомендуется заключить между вызовами методов Beginlnit и Endlnit класса Bitmapimage: bitmap.Beginlnit(): bitmap.UriSource = uri; bitmap.Endlnit(): Как известно, растровые изображения характеризуются шириной и высотой в пикселах. Класс Bitmapimage наследует целочисленные свойства PixelWidth и Pixel- Height, доступные только для чтения, от класса BitmapSource. Растровые изобра- жения часто (хотя и не всегда) содержат информацию о разрешении. Иногда эта информация важна, иногда — нет. Данные о разрешении в точках на дюйм берут- ся из доступных только для чтения свойств DpiX и DpiY типа double. Класс Bitmap Image также содержит доступные только для чтения свойства Width и Height типа double, вычисляемые по следующим формулам: ил- 96 • PixelWidth Width =--------------; DpiX тт . i 96 • Pixel Height Height =-----------2—. DpiY Без множителя 96 в числителе эти формулы возвращают ширину и высоту изображений в дюймах. Умножение преобразует их к аппаратно-независимым единицам. Свойства Width и Height определяют метрические размеры растрового изображения в аппаратно-независимых единицах. Bitmapimage также наследует от BitmapSource свойство Format, содержащее информацию о цветовом формате изображения; если изображение содержит таблицу цветов, для обращения к ней используется свойство Palette. Изображение всегда выводится в окне с максимально возможным размером, но без искажения пропорций. Если пропорции клиентской области не совпада- ют с пропорциями окна, сверху/снизу или слева/справа от изображения будет виден фон клиентской области окна. Размеры изображения в окне определяются несколькими свойствами Image, к числу которых принадлежит и свойство Stretch. По умолчанию свойство Stretch равно Stretch.Uniform; это означает, что изображение увеличивается или умень-
Концепция содержимого 63 шается пропорционально, то есть в равной степени по вертикали и горизонтали. Также можно задать свойству Stretch значение Stretch.Fill: img.Stretch = Stretch.F111; Изображение заполняет всю клиентскую область окна; обычно это приводит к искажению пропорции. В режиме Stretch.UniformToFill пропорции также сохра- няются, но изображение заполняет всю клиентскую область. Этот удивительный результат достигается усечением изображения с одной из сторон. В режиме Stretch.None изображение выводится со своими метрическими размерами, опре- деляемыми свойствами Width и Height класса BitmapSource. Во всех режимах, кроме Stretch.None, также можно задать свойство Stretch- Direction класса Image. По умолчанию используется значение StretchDirection.Both, разрешающее увеличение или уменьшение изображения по отношению к его метрическим размерам. Значение StretchDirection.DownOnly изображения не мо- жет стать больше, а в режиме StretchDirection.UpOnly — меньше своих метрических размеров. Независимо от размеров, изображение почти всегда (кроме Stretch.Uniform- ToFill) размещается в центре окна. Его позиция изменяется при помощи свойств HorizontalAlignment и VerticalAlignment, унаследованных Image от FrameworkElement. Например, следующий фрагмент перемещает изображение в правый верхний угол клиентской области: img.HorizontalAlignment = HorizontalAlignment.Right: Img.VerticalAlignment = VerticalAlignment.Top: Свойства HorizontalAlignment и VerticalAlignment играют на удивление важную роль в WPF; на страницах книги мы еще не раз встретим их. Если вы предпочи- таете, чтобы объект Image находился в правом верхнем углу, но не выравнивал- ся по краям, задайте отступ для объекта Image: img.Margin = new Thlckness(lO); Свойство Margin определяется в FrameworkElement и часто используется для создания свободного пространства вокруг интерфейсных элементов. Структура Thickness может использоваться для определения как полей одинаковой шири- ны (в данном примере 10/96 дюйма), так и разных полей со всех четырех сторон. Изменение свойства Foreground для объекта Window не влияет на вывод изо- бражений. Это свойство учитывается только в том случае, если содержимое окна представляет собой текст или другой тип интерфейсных элементов для отобра- жения текста (см. далее). Обычно изменение свойства Background для объекта Window отражается только на частях клиентской зоны, не закрытых изображением. Но попробуйте выпол- нить следующий фрагмент: Img.Opacity = 0.5; Background = new L1nearGrad1entBrush(Colors.Red. Colors.Blue. new PointCO. 0). new Polnld, D): Теперь кисть просматривается через изображение. Свойство Opacity (насле- дуемое Image от UIEIement) по умолчанию равно 1, по ему можно задать любое
64 Глава 3. Концепция содержимого значение от 0 до 1, чтобы сделать интерфейсный элемент полупрозрачным. (С самим объектом Windows это сделать не удастся.) Полное описание графических преобразований ожидает нас в главе 29, а пока я просто покажу, как легко повернуть растровое изображение: Img.LayoutTransform = new RotateTransform(45): Класс Image не имеет собственных свойств Background и Foreground, потому что эти свойства определяются в классе Control, a Image не является производ- ным от Control. В предыдущих Windows API практически любой экранный объ- ект считался элементом управления (control). В WPF дело обстоит совершенно иначе. Элементы управления составляют особую категорию визуальных объек- тов, характеризуемых в основном способностью реагировать на пользовательский ввод. Конечно, интерфейсные элементы вроде Image тоже могут получать вво- димые пользователем данные благодаря событиям клавиатуры, мыши и стилуса, определяемым в UIEIement. Взгляните на пространство имен System.Windows.Shapes, содержащее абстракт- ный класс Shape и шесть других классов, производных от него. Эти классы так- же являются производными от UIEIement через промежуточный класс Framework- Element: Object Dispatcherobject (абстрактный) Dependencyobject Visual (абстрактный) UIEIement FrameworkElement Shape (абстрактный) Ellipse Line Path Polygon Polyline Rectangle Если Image является стандартным средством вывода растровых изображений, классы Shape реализуют простую двумерную векторную графику. Следующая программа создает объект типа Ellipse. ShapeAnEllipse.cs //----------------------------------------------- // ShapeAnE111pse.cs (с) 2006 by Charles Petzold //----------------------------------------------- using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media:
Концепция содержимого 65 using System.Windows.Shapes: namespace Petzold.ShapeAnEllipse t class ShapeAnEllipse : Window t [STAThread] public static void MalnO { Application app = new Application); app.Run(new ShapeAnEllIpseO); public ShapeAnEllIpseO Title = "Shape an Ellipse"; Ellipse ellps = new ElllpseO; elips.Fill = Brushes.AlIceBlue; ellps.StrokeThlckness = 24; // 1/4 дюйма el Ips.Stroke = new L1nearGrad1entBrush(Colors.CadetBlue, Colors.Chocolate. new Polntd, 0). new Po1nt(0, 1)); Content = ellps; } } Эллипс заполняет всю клиентскую область. Граница имеет толщину в чет- верть дюйма и окрашивается градиентной кистью. Внутренняя часть эллипса закрашивается бледно-голубым цветом. Ни класс Shape, ни класс Ellipse не определяют свойств для задания размеров эллипса. Впрочем, класс Ellipse наследует свойства Width и Height от Framework- Element, и эти свойства отлично работают: ellps.Width = 300; el Ips.Height = 300; Как и в случае с Image, свойства HorizontalAlignment и VerticalAlignment могут использоваться для размещения эллипса по центру, его смещению по вертикали или горизонтали: el Ips.HorizontalAlignment = HorizontalAlignment.Left; ellps.VerticalAlignment = Vertleal Alignment.Bottom; Оба перечисления, HorizontalAlignment и VerticalAlignment, содержат элементы с именем Center, но в них также присутствует элемент с именем Stretch, исполь- зуемый по умолчанию для многих интерфейсных элементов — в частности, для Ellipse. Именно по этой причине Ellipse изначально заполняет клиентскую область. Интерфейсный элемент растягивается по границам своего контейнера. Если задать HorizontalAlignment или VerticalAlignment значение, отличное от Stretch, без явного задания свойств Width и Height, эллипс сворачивается в крошечный шарик,
66 Глава 3. Концепция содержимого состоящий из одной границы. С другой стороны, если свойства Width и Height не заданы, вы можете задать любые из свойств MinWidth, MaxWidth, MinHeight и Max- Height (унаследованных от FrameworkElement), чтобы размеры эллипса находились в определенных границах. По умолчанию все эти свойства не определяются. Программа может в любой момент (кроме конструктора окна) получить факти- ческие размеры эллипса из свойств Actualwidth и ActualHeight. Если я уделяю много внимания теме размера элементов, заданных в качестве содержимого окна, то это объясняется только ее важностью. Вероятно, вы при- выкли назначать конкретные размеры элементов управления и других графиче- ских объектов, но в WPF этот подход не обязателен, поэтому вы должны при- выкнуть к новому способу определения размеров визуальных объектов. Вы не найдете в классе Ellipse свойств, позволяющих нормально разместить эллипс в произвольной точке клиентской области окна. Вероятно, лучшее, что можно предложить на этой стадии, — настройка свойств HorizontalAlignment и Ver- ticalAlignment. Ранее я уже показал, как задать свойству Content класса Window произвольную текстовую строку и как изменить шрифт этого текста. Однако текст, напрямую заданный свойству Content окна, должен обладать единым форматированием. Например, в нем нельзя выделить отдельные слова жирным или курсивным на- чертанием. Как решить эту задачу? Вместо того чтобы задавать свойству Content строку, следует задать ему объект типа TextBlock: FormatTheText.cs //---------------------------------------------- // FormatTheText.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Documents; namespace Petzold.FormatTheText class FormatTheText : Window { [STAThread] public static void MalnO { Application app = new Application); app.Run(new FormatTheTextO); } public FormatTheTextO
Концепция содержимого 67 Title = "Format the Text"; TextBlock txt = new TextBlockO; txt.FontSize =32; // 24 пункта txt.Inlines.Add("This is some "); txt.Inlines.Add(new Italic(new Run("itaiic"))); txt.Inlines.Add(" text, and this is some "); txt.Ini Ines.Add(new Bold(new RunC'bold"))); txt.Ini Ines.Add(" text, and let’s cap it off with some "); txt.Inlines.Add(new Bold(new Italic(new RunC'bold italic")))); txt.Ini Ines.Add(" text."); txt.Textwrapping = Textwrapping.Wrap; Content = txt; } } } Хотя это первая программа в книге, явно создающая объект TextBlock, на са- мом деле этот объект нам уже встречался. Когда мы задавали свойству Content строку, класс Contentcontrol (от которого наследует Window) создает объект типа TextBlock для вывода строки. Класс TextBlock является производным непосредст- венно от FrameworkElement. Он определяет свойство Inlines типа InlineCollection (коллекция объектов Inline). Сам класс TextBlock входит в пространство имен System.Windows.Controls, но Inline является частью пространства System.Windows.Documents, a UIEIement даже не входит в число его предков. Частичная иерархия классов с Inline и некоторы- ми его потомками; Object Dispatcherobject (абстрактный) Dependencyobject ContentElement FrameworkContentElement TextElement (абстрактный) Inline (абстрактный) Run Span Bold Hyperlink Italic Underline По своей структуре она отдаленно напоминает уже встречавшиеся иерархии. Классы ContentElement и FrameworkContentElement аналогичны классам UIEIement и FrameworkElement. Впрочем, класс ContentElement не содержит метода OnRender. Объекты классов, производных от ContentElement, не прорисовывают себя на экране. Их визуальное представление обеспечивается только классом, производ- ным от UIEIement, предоставляющим необходимый метод OnRender. Если говорить
68 Глава 3. Концепция содержимого более конкретно, объекты типов Bold и Italic не перерисовывают себя. В програм- ме FormatTheText эти объекты Bold и Italic воспроизводятся объектом TextBlock. Не путайте класс ContentElement с Contentcontrol. Contentcontrol — элемент управ- ления (в том числе и Window), содержащий свойство с именем Content. Объект Contentcontrol воспроизводит себя на экране даже в том случае, если его свойст- во Content равно null, а объект ContentElement для воспроизведения должен быть частью (вернее, содержимым) другого элемента. Основной объем программы FormatTheText занимает построение коллекции Inlines объекта TextBlock. Класс InlineCollection реализует методы Add для объектов string, Inline и UIEIement (последний позволяет внедрять другие интерфейсные элементы в TextBlock). Однако конструкторы Bold и Italic принимают только объ- екты Inline, но не объекты string, поэтому программа сначала использует конст- руктор Run для каждого объекта Bold и Italic. Программа задает свойство FontSize объекта TextBlock: txt.FontSize = 32: Точно так же она будет работать и при задании свойства FontSize окна: FontSize = 32: Аналогично, при задании свойства Foreground окна текст TextBlock выводится заданным цветом: Foreground = Brushes.CornflowerBlue; Интерфейсные элементы на экране объединены в дерево иерархий «роди- тель-потомок». Окно является родителем для объекта TextBlock, который явля- ется родителем для нескольких элементов Inline. Потомки наследуют значения свойства Foreground и всех шрифтовых свойств от своих родительских элемен- тов, если только эти свойства не были явно заданы для потомков. В главе 8 бу- дет подробно показано, как работает эта схема. Класс ContentElement, как и UIEIement, определяет ряд событий пользователь- ского ввода. Обработчики событий можно связать и с отдельными элемента- ми Inline, составляющими текст TextBlock; следующий пример показывает, как это делается. ToggleBoldAndItalic.cs // // ToggleBoldAndItalic.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls: using System.Windows.Documents; using System.Windows.Input: using System.Windows.Media; namespace Petzold.ToggleBoldAndlta1ic
концепция содержимого 69 public class ToggleBoldAndltalic : Window [STAThread] public static void MainO Application app = new ApplicationO; app.Run(new ToggleBoldAndltallcO): } public ToggleBoldAndltallcO { Title = "Toggle Bold & Italic"; TextBlock text = new TextBlockO; text.FontSize = 32; text.HorizontalAlignment = HorizontalAlignment.Center; text.VerticalAlignment = VerticalAlignment.Center; Content = text; string strQuote = "To be, or not to be, that is the question"; string[] strWords = strQuote.Split(); foreach (string str in strWords) { Run run = new Run(str); run.MouseDown += RunOnMouseDown; text.Inlines.Add(run); text.Inlines.Add(" "); } } void RunOnMouseDown(object sender, MouseButtonEventArgs args) { Run run = sender as Run; if (args.ChangedButton == MouseButton.Left) run.Fontstyle = run.Fontstyle == Fontstyles.Italic ? Fontstyles.Normal : Fontstyles.Italic; if (args.ChangedButton == MouseButton.Right) run.FontWeight = run.FontWeight == FontWeights.Bold ? FontWeights.Normal ; FontWeights.Bold; } } } Конструктор разбивает знаменитую фразу Гамлета на слова, создает объект Run для каждого слова, после чего снова объединяет слова в коллекцию Inlines объекта TextBlock. При этом программа также связывает обработчик RunOnMouse- Down с событием MouseDown каждого объекта Run.
70 Глава 3. Концепция содержимого Класс Run наследует свойства Fontstyle и FontWeight от класса TextElement, а об- работчик события изменяет эти свойства в зависимости от того, какой кнопкой мыши щелкнул пользователь. Для левой кнопки, если свойство Fontstyle в на- стоящее время равно FontStyles.Italic, обработчик задает ему значение Fontstyles. Normal. Если cbopictbo равно FontStyles.Normal, обработчик заменяет его на Font- Styles.Italic. Аналогичным образом свойство FontWeight переключается между Font- Weights.Normal и FontWeights.Bold. Ранее уже упоминалось, что свойству Content окна на практике должен зада- ваться экземпляр класса, производного от UIEIement, потому что в этом классе определяется метод OnRender, обеспечивающий визуальное представление объ- екта на экране. Последняя программа этой главы — RenderTheGraphic — состоит из двух файлов с исходным кодом. В первом файле содержится класс, опреде- ляющий пользовательский интерфейсный элемент. Второй файл задает экземп- ляр этого класса в качестве содержимого окна. Класс объявляется производ- ным от FrameworkElement — единственного класса, производного непосредствен- но от UIEIement. В переопределении важнейшего метода OnRender класс получает объект Drawingcontext, используемый для рисования эллипса вызовом DrawEllipse. В сущности, класс является несложной имитацией класса Ellipse из пространства имен System.Windows.Shapes. SimpleEllipse.es // // SimpleEllipse.es (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Media; namespace Petzold.RenderTheGraphic { class SimpleEllipse : FrameworkElement protected override void OnRender(DrawingContext de) { dc.DrawEllipse(Brushes.Blue, new Pen(Brushes.Red. 24), new Point(RenderSize.Width / 2, RenderSize.Height / 2), RenderSize.Width / 2, RenderSize.Height /2); } } } Свойство RenderSize определяется перед вызовом OnRender на основании воз- можных значений Width и Height, а также согласования между классом и контей- нером, в котором он отображается. Возникает вроде бы логичное предположение, что метод сразу же рисует эл- липс на экране, но в действительности это не так. Аргументы DrawEllipse сохраня- ются, а рисование эллипса откладывается «на потом». «Потом» может означать
Концепция содержимого 71 «прямо сейчас», но только благодаря сохранению графических операций от раз- ных источников и их объединению на экране WPF совершает большую часть своих графических чудес. Следующая программа создает объект SimpleEllipse и задает его свойству Content. RenderTheGraphic.cs // // RenderTheGraphic.cs (с) 2006 by Charles Petzold //--------------------------------------------------- using System; using System.Windows: namespace Petzold.RenderTheGraphi c { class RenderTheGraphic : Window { [STAThread] public static void MainO { Application app = new Application); app.Run(new ReplaceMainWindowO): } public ReplaceMainWindowO { Title = "Render the Graphic"; SimpleEllipse elips = new SimpleEllipseO; Content = elips; } } Эллипс заполняет клиентскую область. Конечно, при желании вы можете по- экспериментировать со свойствами Width/Height и HorizontalAlignment/VerticalAlign- ment. В этой главе использовались интерфейсные элементы из пространства имен System.Windows.Controls, в ней не встречались классы, производные от Control (кро- ме Window, конечно). Последние предназначены для получения входных данных от пользователя и их реального использования. О том, как это делается, будет рассказано в следующей главе.
Кнопки и другие элементы управления В WPF термин «элемент управления» приобрел более специализированный смысл, чем в предыдущих интерфейсах прикладного программирования Win- dows. Например, в Windows Forms любой визуальный объект на экране счита- ется какой-либо разновидностью элемента управления. В WPF этим термином обозначаются интерактивные интерфейсные элементы, то есть каким-либо об- разом реагирующие на действия с мышью или нажатия клавиш. Так, объекты TextBlock, Image и Shape (см. главу 3) получают ввод с клавиатуры, мыши и сти- луса, хотя и предпочитают игнорировать его. Элементы управления активно отслеживают и обрабатывают пользовательский ввод. Класс Control является производным непосредственно от FrameworkElement: Object Dispatcherobject (абстрактный) Dependencyobject Visual (абстрактный) UIElement FrameworkElement Control Класс Window наследует от Control через промежуточный класс Contentcontrol, поэтому мы уже встречали некоторые свойства, добавляемые Control в Frame- workElement. К числу свойств, определяемых Control, относятся свойства Back- ground, Foreground, BorderBrush, BorderThickness, а также некоторые шрифтовые свойства вроде FontWeight и Stretch. (Класс TextBlock тоже поддерживает ряд шрифтовых свойств, но не является производным от Control — он определяет эти свойства самостоятельно.) Класс Control является базовым для более 50 других классов, реализующих классические элементы управления: кнопки, списки, полосы прокрутки, тексто- вые поля, меню и панели инструментов. Эти классы находятся в пространствах имен System.Windows.Controls и System.Windows.Controls.Primitives наряду с другими классами, которые не являются производными от Control. Прародителем элементов управления является кнопка, представленная в WPF классом Button. Класс Button обладает свойством Content и событием Click, кото- рое срабатывает при нажатии кнопки пользователем (клавишей или щелчком).
Кнопки и другие элементы управления 73 Следующая программа создает объект Button и устанавливает обработчик со- бытия Click, отображающий окно сообщения в ответ на щелчки мышью. CllckTheButton.cs // С11ckTheButton.es (с) 2006 by Charles Petzold и using System: using System.Windows: using System.Windows.Controls; using System.Windows.Input: using System.Windows.Media: namespace Petzold.ClickTheButton public class ClickTheButton : Window t [STAThread] public static void MainO { Application app = new Application); app.Run(new C11ckTheButton()); } public Cl1ckTheButton() Title = "Click the Button"; Button btn = new Button 0; btn.Content = "_C11ck me, please!"; btn.Click += ButtonOnCUck; Content = btn; } void ButtonOnC11ck(object sender, RoutedEventArgs args) MessageBox.ShowC'The button has been clicked and all Is well.", Title); Свойству Content объекта Button задается текстовая строка, а сам объект Button задается свойству Content объекта Window. Порядок выполнения этих двух опе- раций особой роли не играет: вы можете задать свойства кнопки и присоеди- нить обработчик события как до, так и после того, как кнопка станет частью окна. Наличие у Button свойства Content (как и у Window) — не случайное совпадение. Оба класса (а также ряд других) являются производными от класса Contentcontrol,
74 Глава 4. Кнопки и другие элементы управления в котором определяется Content. Вот как выглядит часть иерархии классов, на- чиная с Control: Control Contentcontrol ButtonBase (абстрактный) Button Window Наличие у классов Window и ButtonBase свойства Content приводит к одному принципиальному следствию. Любые разновидности объектов, которые могут использоваться в качестве содержимого окна, также могут использоваться в ка- честве содержимого кнопки. Следовательно, на кнопке можно вывести растро- вое изображение, объект Shape или отформатированный текст. Более того, со- держимым объекта Button даже можно назначить другой объект Button. Вероятно, теперь вас уже не удивит кнопка, которая занимает всю клиент- скую область окна и автоматически увеличивается или уменьшается с измене- нием размеров окна. Изначально кнопка не имеет фокуса ввода, то есть не получает нажатия кла- виш. Например, если запустить программу и нажать пробел, ничего не про- изойдет. Впрочем, даже если кнопка не имеет фокуса ввода, ее можно активи- зировать нажатием клавиш Alt+C. Если присмотреться повнимательнее, можно заметить, что первая буква надписи Click me, please! подчеркивается при нажатии клавиши Alt. Чтобы создать подобную комбинацию ускоренного вызова, следует поставить символ подчеркивания (_) перед соответствующей буквой текстовой строки, заданной свойству Content. Такая возможность обычно применяется для меню, но при использовании с кнопками и другими элементами управления она расширяет возможности управления диалоговыми окнами с клавиатуры. Чтобы передать кнопке фокус ввода, нажмите клавишу Tab или щелкните на кнопке левой кнопкой мыши. Внутри кнопки появляется пунктирная рамка — признак фокуса ввода. После этого кнопку можно активизировать нажатием кла- виши «пробел» или Enter. Передачу фокуса также можно реализовать и на программном уровне; вклю- чите следующую команду в конструктор класса окна: btn.FocusO: Даже если кнопка не обладает фокусом ввода, она среагирует на нажатие Enter, если назначить ее «кнопкой по умолчанию»: btn.IsDefault = true: Другое свойство обеспечит реакцию на нажатие Escape: btn.IsCancel = true: Эти два свойства обычно используются для кнопок ОК и Cancel в диалоговых окнах. Иногда оба свойства устанавливаются для одной кнопки — чаще всего это делается в диалоговых окнах About, содержащих только одну кнопку ОК.
Кнопки и другие элементы управления 75 Присмотритесь повнимательнее и вы увидите, что граница кнопки немного изменяется при прохождении над ней указателя мыши. Кроме того, кнопка меняет свой цвет фона при щелчке. Эти признаки визуальной обратной связи отличают элементы управления от других интерфейсных элементов. В классе ButtonBase определяется свойство ClickMode, принимающее значения из перечисления ClickMode. Свойство управляет реакцией кнопки на щелчки мы- шью. По умолчанию используется значение ClickMode. Release — оно означает, что событие Click инициируется лишь после отпускания кнопки мыши. Другие возмож- ные значения — ClickMode.Press и ClickMode.Hover; в последнем случае событие сра- батывает даже в том случае, если указатель мыши просто проходит над кнопкой. Кнопка в нашем примере получилась просто огромной — она заполняет всю клиентскую область окна. Как было показано в предыдущей главе, класс Frame- workElement определяет свойство с именем Margin, при помощи которого обычно создаются небольшие отступы вокруг элемента: btn.Margin = new Thickness(96): Вставьте эту команду, перекомпилируйте программу — теперь кнопка окру- жена дюймовыми отступами со всех сторон. Конечно, кнопка все равно остается довольно большой, но мы воспользуемся этим обстоятельством для изменения ее внутреннего оформления. Содержимое кнопки — текстовая строка — сейчас раз- мещается в центре. Следующие две команды перемещают ее в левый нижний угол: btn.HorizontaIContentAlignment = HorizontalAlignment.Left: btn.VerticalContentAlignment = VerticalAlignment.Bottom: Свойства HorizontalAlignment и VerticalAlignment рассматривались в главе 3. Обра- тите внимание на слово Content в середине имен свойств: оно означает, что вырав- нивание относится к содержимому кнопки. Как уже говорилось ранее, перечис- ление HorizontalAlignment состоит из четырех элементов: Center, Left, Right и Stretch, а перечисление VerticalAlignment — из элементов Center, Top, Bottom и Stretch. Кнопку также можно снабдить внутренними отступами. Для задания свойст- ва Padding используется та же структура Thickness, что и для свойства Margin: btn.Padding = new Thickness(96): Теперь внутренняя область кнопки отделяется от ее содержимого дюймовы- ми «полями». Свойство Margin (определяемое в FrameworkElement) создает отступы снаружи кнопки, а свойство Padding (определяемое в Control) — внутри нее. Подобно тому как свойства HorizontalContentAlignment и VerticalContentAlignment влияют на размещение содержимого внутри кнопки, свойства HorizontalAlignment и VerticalAlignment определяют местоположение кнопки внутри ее контейнера (клиентской области). Для кнопок эти свойства по умолчанию задаются равными HorizontalAlignment.Stretch и VerticalAlignment.Stretch, поэтому кнопка «растягивает- ся» по размерам клиентской области окна. Зададим оба свойства равными Center: btn.Horizontal Alignment = HorizontalAlignment.Center: btn.VerticalAlignment = Vertical Alignment.Center:
76 Глава 4. Кнопки и другие элементы управления Теперь размер кнопки определяется исключительно ее содержимым (и внут- ренними отступами). Внешние отступы тоже соблюдаются, даже если это и не очевидно на первый взгляд. Чтобы убедиться в этом, можно задать Horizontal- Alignment и VerticalAlignment значения, отличные от Center: btn.HorizontalAlignment = HorizontalAlignment.Left; btn.VerticalAlignment = VerticalAlignment.Top; Если ширина внешних отступов равна 0, кнопка размещается в углу клиент- ской области. При увеличении отступов кнопка отдаляется от угла. Теперь удалим весь экспериментальный код, за исключением команд btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; В этом варианте размеры кнопки автоматически выбираются по размерам ее содержимого (даже если последние изменятся после отображения кнопки), а кнопка размещается в центре окна. А если вдруг возникнет такая необходи- мость, размер кнопки можно задать явно; btn.Width = 96; btn.Height = 96: Конечно, чем меньше вы задаете размер элементов, тем лучше будет резуль- тат. Помните, что WPF старается подгонять размеры содержимого под разреше- ние экрана, выбранное конкретным пользователем. Явное указание размеров элементов только помешает в этом. По умолчанию для вывода текста на кнопках используется шрифт Tahoma с размером И аппаратно-независимых единиц (8 1/4 пункта). И размер, и гар- нитуру можно легко сменить: btn.FontSize = 48; btn.FontFamily = new FontFamily("Times New Roman"); Возможно и другое решение — задать свойства FontSize и FontFamily для объ- екта Window. Кнопка унаследует заданные значения (хотя унаследованные свой- ства не заменяют свойств, явно заданных для объекта Button). И конечно, вы мо- жете полностью изменить цветовую схему кнопки: btn.Background = Brushes.AlIceBlue; btn.Foreground = Brushes.DarkSalmon; btn.BorderBrush = Brushes.Magenta; Свойство BorderThickness у кнопок не используется. Изменять фон кнопки без необходимости не рекомендуется, потому что кнопка перестает предостав- лять визуальную обратную связь при щелчках. С другой стороны, возможно, вы предпочтете организовать собственный ва- риант обратной связи — скажем, когда указатель мыши входит в границы облас- ти над кнопкой. Следующая программа использует объект TextBlock для вывода отформатированного текста на кнопке, но цвет выводимого текста изменяется в ответ на события MouseEnter и MouseLeave:
Кнопки и другие элементы управления 77 FormatTheButton.cs И------------------------------------------------- // FormatTheButton.cs (с) 2006 by Charles Petzold // using System: using System.Windows: using System.Windows.Controls: using System.Windows.Documents: using System.Windows.Input: using System.Windows.Media: namespace Petzold.FormatTheButton { public class FormatTheButton : Window { Run runButton; [STAThread] public static void MainO { Application app = new Application): app.Run(new FormatTheButton)); } public FormatTheButton0 { Title = "Format the Button"; // Создание объекта Button, назначаемого содержимым окна Button btn = new Button); btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.MouseEnter += ButtonOnMouseEnter; btn.MouseLeave += ButtonOnMouseLeave; Content = btn; // Создание объекта TextBlock, назначаемого содержимым кнопки TextBlock txtblk = new TextBlockO; txtblk.FontSize = 24; txtblk.TextAlignment = TextAlignment.Center; btn.Content = txtblk; // Добавление отформатированного текста к TextBlock txtblk. Inlines. Add (new Italic(new RunCClick"))); txtblk.Inlines.Add(" the "); txtblk.Inlines.Add(runButton = new Run("button")); txtblk.Inlines.Add(new LineBreakO); txtblk.Inlines.Add("to launch the "); txtblk.Inlines.Add(new Bold(new Run("rocket"))); }
78 Глава 4. Кнопки и другие элементы управления void ButtonOnMouseEnterCobject sender, MouseEventArgs args) { runButton.Foreground = Brushes.Red; } void ButtonOnMouseLeave(object sender. MouseEventArgs args) { runButton.Foreground = SystemColors.ControlTextBrush; } } } Обратите внимание: объект Run co словом button хранится в поле с именем runButton. Когда указатель мыши входит в границы кнопки, свойству Foreground объекта Run задается красный цвет. При выходе указателя за пределы кнопки восстанавливается цвет текста по умолчанию, полученный из SystemColors.Control- TextBrush. Следующая программа рисует кнопку с графическим изображением вместо текста. В данном примере изображение не загружается по Интернету или из внешнего графического файла — оно встраивается прямо в исполняемый файл в виде ресурса. Слово ресурс в WPF имеет несколько значений. Ресурсы того типа, о котором идет речь, иногда называются ресурсами сборки. Такой ресурс представляет собой файл (как правило, двоичный), который является частью проекта Visual Studio, и встраивается в исполняемый файл или DLL-библиотеку. Для обращения к файлу в программе используется следующий конструктор Uri (замените файл именем файла): Uri uri = new Uri("pack.-//application:, ,/файл"); Обратите внимание на две запятые и символ / перед именем файла. Если проект содержит несколько графических файлов, для их хранения удоб- но выделить отдельную папку проекта (например, Images). Синтаксис загрузки графики в программу приобретает вид Uri uri = new Uri("pack://application:,,/Images/файл"); Обратите внимание на символ / перед именем папки. В следующем примере на кнопке выводится знаменитое изображение крича- щего человека с картины Эдварда Мунка «Крик». ImageTheButton.es //---------------------------------------------- // ImageTheButton.cs (с) 2006 by Charles Petzold //---------------------------------------------- using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: using System.Windows.Media.Imaging:
Кнопки и другие элементы управления 79 namespace Petzold.ImageTheButton ( public class ImageTheButton : Window { [STAThread] public static void Main!) { Application app = new Application!): app.Run(new ImageTheButton!)): public ImageTheButton!) { Title = "Image the Button": Uri uri = new Uri("pack://applIcatlon:,,/munch.png"); Bitmapimage bitmap = new B1tmaplmage(ur1): Image Img = new ImageO: Img.Source = bitmap: Img.Stretch = Stretch.None: Button btn = new ButtonO; btn.Content = Img: btn.Horizontal Alignment = HorizontalAlignment.Center: btn.VerticalAlignment = VerticalAlignment.Center: Content = btn; } } } Попробуйте использовать в программе файл, содержащий прозрачные участ- ки; вы увидите за ними обычный цвет фона. Конечно, было бы хорошо вывести на кнопке изображение вместе с текстом, но при этом возникает уже знакомая проблема: свойству Content может зада- ваться только один объект. Ограничение раздражает, но решение проблемы уже не за горами. Наиболее традиционным способом оповещения о щелчках на кнопке являет- ся установка обработчика для события Click. Однако может оказаться, что неко- торая операция программы инициируется несколькими источниками — коман- дой меню, кнопкой панели инструментов, клавишей и даже сценарием. Было бы логично свести все пути в одну точку. Эта идея заложена в основу свойства Command, определяемого классом Button- Base и другими классами, включая Menuitem. Для обычных команд свойство Command объекта Button задается равным статическому свойству классов Applica- tionCommands, Componentcommands, Mediacommands, Navigationcommands или Editing- Commands. Первые четыре класса находятся в пространстве имен System.Windows. Input, а последний — в пространстве System.Windows.Documents. Все статические свойства этих классов относятся к типу RoutedUICommand. Вы также можете со- здавать собственные объекты типа RoutedUICommand (см. главу 14).
80 Глава 4. Кнопки и другие элементы управления Например, чтобы некоторая кнопка выполняла команду вставки из буфера обмена, ее свойство Command задается следующим образом: btn.Command = Applicationcommands.Paste: Текст кнопки обычно заменяется стандартным текстом данной команды, хотя это и не обязательно: btn. Content = AppHcatlonCommands. Paste. Text: Следующим шагом должно стать ассоциирование команды вставки с обра- ботчиками событий. Установление подобных ассоциаций называется привязкой (binding); это один из нескольких типов привязок, встречающихся в WPF. Класс UIEIement определяет (а классы Control и Window наследуют) свойство Command- Bindings, которое представляет собой коллекцию объектов CommandBinding. Веро- ятно, в программах стоит использовать коллекцию CommandBindings окна, поэто- му команда будет выглядеть так: CommandBInd1ngs.Add(new CommandBIndlng(Appl1 cat1onCommands.Paste, PasteOnExecute, PasteCanExecute)); PasteOnExecute и PasteCanExecute — два обработчика событий, находящиеся в вашей программе (имена выбираются произвольно). В обработчике PasteOn- Execute выполняется команда вставки данных. Однако в некоторых ситуациях приложение не может выполнить команду вставки, потому что буфер обмена не содержит данных в нужном формате. Именно для этой цели предназначен обра- ботчик PasteCanExecute — он проверяет формат данных в буфере обмена. Если данные недоступны, обработчик устанавливает флаг и кнопка автоматически блокируется (становится недоступной для пользователя). Если бы в той же программе существует команда меню для вставки данных, достаточно задать свойству Command этой команды меню Applicationcommands. Paste, и к ней будет применена та же привязка. Привязка команд продемонстрирована в следующей простой программе. CommandTheButton.cs //------------------------------------------------ // CommandTheButton.cs (с) 2006 by Charles Petzold //------------------------------------------------ using System: using System.Windows; using System.Windows.Controls; using System.Windows.Input: using System.Windows.Media; namespace Petzold.CommandTheButton { public class CommandTheButton ; Window { [STAThread]
Кнопки и другие элементы управления 81 public static void MalnO Application app = new Application); app.Run(new CommandTheButtonO); } public CommandTheButtonO { Title = "Command the Button": // Создание объекта Button, назначаемого содержимым окна Button btn = new ButtonO; btn.HorizontalAlignment = HorizontalAlignment.Center: btn.VerticalAlignment = VerticalAlignment.Center; btn.Command = Applicationcommands.Paste: btn.Content = Applicationcommands.Paste.Text: Content = btn: // Привязка команды к обработчикам событий CommandBind1ngs.Add(new CommandBi ndi ng( Appli cati onCommands.Paste, PasteOnExecute, PasteCanExecute)): } void PasteOnExecute(object sender, ExecutedRoutedEventArgs args) { Title = Clipboard.GetText(): void PasteCanExecute(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = Clipboard.ContainsTextO: } protected override void OnMouseDown(MouseButtonEventArgs args) { base.OnMouseDown(args): Title = "Command the Button": } } Конструктор окна задает значение ApplicationCommands.Paste свойству Command объекта Button. Работа конструктора завершается созданием объекта Command- Binding, связывающего ApplicationCommands.Paste с обработчиками событий Paste- OnExecute и PasteCanExecute. Объект CommandBinding становится частью коллек- ции CommandBindings окна. Обработчики PasteOnExecute и PasteCanExecute используют статические ме- тоды класса Clipboard из пространства имен System.Windows. Статические методы GetText и ContainsText существуют в перегруженных версиях, которые позволя- ют точно указать тип текста (разделение запятыми, HTML, RTF, Юникод или XAML), но в данной программе эти типы не используются. Текст, полученный из буфера, просто задается свойству Title окна. Если вам захочется немного
82 Глава 4. Кнопки и другие элементы управления поэкспериментировать с программой, последний метод позволяет восстановить текст Title щелчком в любом месте окна. Перед запуском программы скопируйте какой-нибудь текст из Блокнота или другого текстового окна (даже из Visual Studio), чтобы у программы были дан- ные для работы. Вы увидите, что кнопка реагирует не только на обычный ввод с клавиатуры и мыши, но и на Ctrl+V — стандартную комбинацию клавиш для выполнения вставки. Это одно из преимуществ привязки команд в сравнении с простой обработкой события Click. Теперь скопируйте в буфер растровое изображение. Не обязательно запус- кать Paint или PhotoShop — достаточно нажать клавишу PrintScreen. При каж- дом изменении содержимого буфера вызываются все привязанные обработчики *CanExecute (например, PasteCanExecute), чтобы обработчик мог задать свойство CanExecute аргумента. Кнопка немедленно блокируется. Теперь скопируйте в бу- фер текст — кнопка снова становится доступной. Хотя кнопки в приложениях обычно предназначены для выполнения команд (через обработчики событий Click или привязки команд), существуют особые разновидности кнопок для обозначения альтернативных вариантов. Флажок — элемент CheckBox — состоит из квадратного поля, за которым обычно следует пояснительный текст. Наличие или отсутствие маркера («галочки») в поле обо- значает состояние флажка (установлен/снят). Например, диалоговое окно для выбора шрифта может содержать элементы CheckBox для жирного и курсивного начертания, а также подчеркивания. Переключатели — элемент RadioButton — обычно используются для представ- ления групп взаимоисключающих вариантов. И CheckBox, и RadioButton относят- ся к общей категории «кнопок с двумя состояниями» (ToggleButton), как видно из дерева классов, производных от ButtonBase: Control Contentcontrol ButtonBase (абстрактный) Button Gri dVi ewColumnHeader RepeatButton ToggleButton CheckBox RadioButton Класс ToggleButton не является абстрактным. Вы можете создать объект типа ToggleButton, который выглядит и работает как обычная кнопка, но имеет два со- стояния: включенное и выключенное. В следующей программе объект Toggle- Button используется для изменения свойства ResizeMode окна. Togg leTheButton .cs //----------------------------------------------- // ToggleTheButton.cs (с) 2006 by Charles Petzold // using System;
Кнопки и другие элементы управления 83 using System.Windows: using System.Windows.Controls: usi ng System.Wi ndows.Controls.Primi ti ves; using System.Windows.Input: using System.Windows.Media: namespace Petzold.ToggleTheButton ( public class ToggleTheButton : Window { [STAThread] public static void MainO { Application app = new ApplicationO: app.Run(new ToggleTheButton!)); } public ToggleTheButton!) { Title = "Toggle the Button"; ToggleButton btn = new ToggleButtonO; btn.Content = "Can _Resize"; btn.Horizontal Alignment = Horizontal Alignment.Center; btn.Vertical Alignment = VerticalAlignment.Center; btn.IsChecked = (ResizeMode == ResizeMode.CanResize); btn.Checked += ButtonOnChecked; btn.Unchecked += ButtonOnChecked; Content = btn; } void ButtonOnChecked(object sender, RoutedEventArgs args) { ToggleButton btn = sender as ToggleButton; ResizeMode = (bool)btn.IsChecked ? ResizeMode.CanResize : ResizeMode.NoResize: } } } Кнопка используется приложением для переключения свойства ResizeMode между значениями ResizeMode.CanResize и ResizeMode.NoResize. Кнопка автомати- чески переходит в противоположное состояние по нажатию. Свойство IsChecked обозначает текущее состояние. Обратите внимание: программа инициализирует IsChecked равным true, если свойство ResizeMode изначально равно ResizeMode. CanResize, и false в противном случае. При «включении» или «выключении» кнопки срабатывают события Checked и Unchecked. Один обработчик можно свя- зать с обоими событиями и различать их по свойству IsChecked — именно этот способ выбран в приложении ToggleTheButton. Обратите внимание: перед использованием в условном операторе C# свойство IsChecked необходимо преобразовать к типу bool. IsChecked в действительности
84 Глава 4. Кнопки и другие элементы управления может находиться в трех состояниях: истинном, ложном и неопределенном. Если свойству IsThreeState объекта ToggleButton присвоить истинное значение, то IsChe- cked можно будет присвоить null для обозначения третьего, «неопределенного» состояния кнопки. Например, кнопка курсивного начертания в текстовом редак- торе может находиться в неопределенном состоянии, если выделенный фрагмент текста содержит как курсивные, так и обычные символы. (Если же текущий фрагмент оформлен шрифтом, не поддерживающим курсив, программа блоки- рует кнопку, задавая ее свойству IsEnabled значение false.) Класс CheckBox почти ничего не добавляет в ToggleButton. Чтобы создать в про- грамме ToggleTheButton объект CheckBox, достаточно заменить код создания кнопки следующей командой: CheckBox btn = new CheckBoxO; Вам даже не придется изменять обработчик события. Программа работает точно так же, как прежде, хотя кнопка выглядит иначе. Также можно заменить кнопку переключателем RadioButton, но тогда программа работать перестает — невозможно изменить состояние кнопки RadioButton простым щелчком. Поскольку объекты ToggleButton и CheckBox фактически представляют значе- ние Boolean, было бы вполне логично ассоциировать их с некоторым логическим свойством объекта. Подобная ассоциация называется привязкой данных. Меха- низм привязки данных играет настолько важную роль в WPF, что глава 23 пол- ностью посвящена этой теме. Объект ToggleButton, используемый для привязки данных, не обязан иметь инициализированное свойство IsChecked или установленные обработчики собы- тий Checked и Unchecked. Вместо этого требуется лишь вызов метода SetBinding, унаследованного кнопкой от класса FrameworkElement. Для объектов ToggleButton или CheckBox вызов обычно выглядит примерно так: btn.SetBinding(ToggleButton.IsCheckedProperty, "SomeProperty"): Второй аргумент содержит строку с именем свойства, ассоциируемого с со- стоянием кнопки IsChecked. Объект, которому принадлежит SomeProperty, указы- вается в свойстве DataContext объекта Button. Вероятно, первый аргумент метода SetBinding выглядит более странно, чем вто- рой. Он ссылается не на свойство IsChecked объекта ToggleButton, а на некий ста- тический член класса ToggleButton с именем IsCheckedProperty. Класс ToggleButton действительно определяет статическое поле с именем Is- CheckedProperty, которое представляет собой объект типа Dependencyproperty. Гла- ва 8 полностью посвящена объектам Dependencyproperty; пока будем считать, что это просто удобный способ ссылки на конкретное свойство, определяемое ToggleButton. Итак, к какому же логическому свойству мы привяжем ToggleButton? Удоб- ным примером может послужить свойство Topmost окна. Если свойство Topmost истинно, окно всегда отображается на переднем плане, поверх других окон. Вы- зов SetBinding может выглядеть так: btn.SetВinding(ToggleButton.IsCheckedProperty. "Topmost");
Кнопки и другие элементы управления 85 Объект, которому принадлежит свойство Topmost, должен быть указан явно, для чего свойству DataContext кнопки задается объект окна: btn.DataContext = this: Полный код программы выглядит так. BindTheButton.cs // // BindTheButton.cs (с) 2006 by Charles Petzold И----------------------------------------------- using System; using System.Windows: using System.Windows.Controls: us i ng System.Wi ndows.Cont rol s.Pr1mi t i ves; using System.Windows.Input: using System.Windows.Media: namespace Petzold.BindTheButton { public class BindTheButton : Window { [STAThread] public static void Main!) Application app = new Application!): app.RunCnew BindTheButton!)); } public BindTheButton!) { Title = "Bind theButton"; ToggleButton btn = new ToggleButton!); btn.Content = "Make _Topmost": btn.Horizontal Alignment = HorizontalAlignment.Center: btn.VerticalAlignment = VerticalAlignment.Center: btn.SetBindingIToggleButton.IsCheckedProperty, "Topmost"); btn.DataContext = this: Content = btn; ToolTip tip = new ToolTip!); tip.Content = "Toggle the button on to make ” + "the window topmost on the desktop": btn.ToolTip = tip: При запуске программы кнопка автоматически инициализируется текущим состоянием свойства Topmost окна. Далее при включении/выключении кнопки свойство будет изменяться.
86 Глава 4. Кнопки и другие элементы управления Пространство имен System,Windows.Data содержит класс Binding, который так- же может использоваться для создания привязки данных. Следующий фрагмент заменяет команды вызова SetBinding и задания свойства DataContext в программе BindTheButton: Binding bind = new Binding("Topmost"): bind.Source = this; btn.SetBinding(ToggleButton.IsCheckedProperty. bind): Как будет показано в главе 23, класс Binding предоставляет больше возмож- ностей для определения привязки данных. Программа BindTheButton также включает объект подсказки ToolTip, который задается свойству ToolTop объекта Button. Чтобы текст появился на экране, до- статочно задержать указатель мыши над кнопкой. Свойство ToolTip определяется классом FrameworkElement, что позволяет присоединять подсказки к интерфейс- ным элементам и элементам управления. Класс ToolTip наследует от Contentcontrol, как и классы ButtonBase с Window, поэтому в подсказках можно использовать форматированный текст и даже графику — если вы хотите окончательно сбить с толку пользователей приложения. Чтобы включить графическое изображение в подсказку, вернитесь к програм- ме ImageTheButton и вставьте следующий фрагмент в конец конструктора: btn.Content = "Don't Click Me!"; ToolTip tip = new ToolTipO; tip.Content = img; btn.ToolTip = tip; На кнопке выводится текст, а в подсказке — предупреждающий рисунок. Еще один простой элемент управления, производный от Contentcontrol — Label, — традиционно использовался для вывода коротких текстовых надписей на формах и в диалоговых окнах. Например, следующая надпись может исполь- зоваться в диалоговом окне открытия файла: Label ТЫ = new Label О: 1 bl.Content = "File _name:"; Может показаться, что с появлением универсальных элементов вроде TextBlock элемент Label устарел, но это не так. «Секрет его долголетия» кроется в возмож- ности включения подчеркиваний в текст, задаваемый свойству Content. Так, на- жатие клавиш Alt+N в упоминавшемся ранее диалоговом окне передает фокус ввода не надписи (так как надписи обычно не могут получать фокус), а эле- менту, следующему за надписью, — вероятнее всего, элементу TextBox для ввода и правки текста. Таким образом, объекты Label, как и прежде, помогают органи- зовать клавиатурную навигацию в диалоговых окнах. Раз уж речь зашла об элементах TextBox, в WPF поддерживаются две разно- видности таких полей: Control TextBoxBase (абстрактный) TextBox RichTextBox
Кнопки и другие элементы управления 87 Хотя элемент TextBox обладает содержимым, он не наследует от Contentcontrol, потому что его содержимое всегда является текстом. Вместо свойства Content у него имеется свойство Text, при помощи которого задается исходная строка или читается текст, введенный пользователем. Так как TextBox наследует от Control, вы можете задать кисти прорисовки фона и текста, а также шрифт. Тем не менее весь текст, отображаемый в отдельном элементе TextBox, всегда оформляется одним цветом и одним шрифтом. Элемент RichTextBox позволяет программе (и пользователю) применять разное формати- рование символов и абзацев к разным фрагментам текста. Элемент TextBox от- личается от RichTextBox примерно тем же, чем Блокнот отличается от WordPad. По умолчанию в элементе TextBox вводится одна строка текста. Следующий пример создает диалоговое окно с одним элементом TextBox. UriDialog.cs И-------------------------------------------- // UriDialog.cs (с) 2006 by Charles Petzold //------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.NavigateTheWeb { class UriDialog : Window { TextBox txtbox; public UriDialogO { Title = "Enter a URI"; ShowInTaskbar = false; SizeToContent = SizeToContent.WidthAndHeight; Windowstyle = Windowstyle.Toolwindow; WindowStartupLocation = WindowStartupLocation.CenterOwner; txtbox = new TextBox(); txtbox.Margin = new Thickness(48); Content = txtbox; txtbox.Focus(); } public string Text { set txtbox.Text = value;
88 Глава 4. Кнопки и другие элементы управления txtbox.Selectionstart = txtbox.Text.Length; } get { return txtbox.Text; } } protected override void OnKeyDown(KeyEventArgs args) { if (args.Key == Key.Enter) CloseO; } } } Класс UriDialog наследует от Window, но не содержит метод Main (этот метод находится в классе главного окна программы), а в начале его конструктора зада- ются значения нескольких свойств. Диалоговые окна не должны отображать- ся на панели задач; обычно их размеры выбираются по размерам содержимого, а свойству Windowstyle задается значение ToolWindow (диалоговое окно допускает изменение размеров, но не имеет кнопок сворачивания/разворачивания). Свой- ство WindowStartupLocation указывает, что диалоговое окно должно располагаться по центру своего владельца (до которого мы еще не дошли). Заголовок предлагает ввести URI, а поле TextBox находится в середине кли- ентской области, готовое принять ввод пользователя. Класс также имеет от- крытое свойство с именем Text. Классы диалоговых окон почти всегда содер- жат открытые свойства, через которые программа взаимодействует с окном. Свойство Text окна просто предоставляет открытый доступ к свойству Text эле- мента TextBox. Обратите внимание на то, как метод доступа set использует свой- ство Selectionstart элемента TextBox для размещения курсора в конце текущего содержимого поля. Пользователь может закрыть диалоговое окно, щелкнув на кнопке закрытия или нажав клавишу Enter, — последняя возможность предо- ставляется переопределением OnKeyDown. Далее приводится пример программы, использующей окно UriDialog. Про- грамма задает свойству Content своего окна объект типа Frame. Класс Frame тоже наследует от Contentcontrol, но у объекта Frame задается свойство Source вместо свойства Content. NavigateTheWeb.cs // // NavigateTheWeb.cs (с) 2006 by Charles Petzold // using System: using System.Windows; using System.Windows.Controls; using System.Windows.Input;
Кнопки и другие элементы управления 89 using System.Windows.Media: namespace Petzold.NavigateTheWeb ( public class NavigateTheWeb : Window { Frame frm: [STAThread] public static void MainO { Application app = new Application!): app.Run(new NavigateTheWeb!)); public NavigateTheWeb!) { Title = "Navigate the Web"; frm = new Frame!): Content = frm: Loaded += OnWindowLoaded; } void OnWindowLoaded(object sender. RoutedEventArgs args) { UriDialog dig = new UriDialogO; dig.Owner = this; dig.Text = "http://": dlg.ShowDialogO; try { frm.Source = new Uri(dig.Text); } catch (Exception exc) { MessageBox.Show!exc.Message, Title): } } } } Программа назначает обработчик для события Loaded. К моменту вызова OnWindowLoaded главное окно уже отображается на экране. На этой стадии окно создает объект типа UriDialog, назначает ему владельца (Owner) и задает свойству Text строку «http://». Вызов ShowDialog отображает диалоговое окно в модаль- ном режиме, а управление возвращается лишь после того, как пользователь за- кроет окно. Если данные были введены верно, текстовое поле будет содержать действительный URI-адрес сайта. Обратите внимание на то, как диалоговое окно
90 Глава 4. Кнопки и другие элементы управления увеличивается в ширину, если URI-адрес не помещается в текстовом поле. Объ- ект TextBox расширяется по размерам текста, а объект Window следует за ним. Когда ShowDialog вернет управление, программа задает свойство Source эле- мента Frame — и мы получаем простейший браузер! Конечно, в нем нет панели с навигационными кнопками, но клавиша Backspace действует как кнопка Назад (Back) в браузере, а щелчок правой кнопкой мыши на веб-странице открывает доступ к другим функциям (включая печать). Диалоговое окно UriDialog закрывается при нажатии клавиши Enter, потому что по умолчанию объект TextBox игнорирует эту клавишу. Если задать свойство txtbox.AcceptsReturn = true; однострочное текстовое поле становится многострочным и начинает обрабаты- вать перевод строки. В следующей программе объект TextBox заполняет клиентскую область окна (возможно, перед нами заготовка для очередного любительского клона Блокно- та). Вместо меню Файл программа использует фиксированное имя и местонахо- ждение файла. Например, с ее помощью можно вводить короткие заметки и со- хранять их между сеансами работы в Windows. EditSomeText.cs // // EditSomeText.cs (с) 2006 by Charles Petzold // using System: using System.ComponentModel; // для CancelEventArgs using System.10; using System.Windows: using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.EditSomeText { class EditSomeText : Window { static string strFileName = Path.Combine( Envi ronment.GetFolderPath( Envi ronment.Speci a1 Folder.Loca1Appli cati onData), "PetzoldWEdi tSomeTextWEditSomeText.txt"); TextBox txtbox: [STAThread] public static void MainO { Application app = new Application); a pp. Run (new EditSomeTextO); } public EditSomeTextO
Кнопки и другие элементы управления 91 { Title = "Edit Some Text"; // Создание текстового поля txtbox = new TextBoxO; txtbox.AcceptsReturn = true; txtbox.Textwrapping = Textwrapping.Wrap; txtbox.VerticalScrol1BarVisi bi 1ity = Scrol1BarVisi bi 1ity.Auto; txtbox.KeyDown += TextBoxOnKeyDown; Content = txtbox; // Загрузка текстового файла try { txtbox.Text = File.ReadAllText(strFileName): } catch { } // Позиционирование курсора и передача фокуса txtbox.CaretIndex = txtbox.Text.Length; txtbox. FocusO; } protected override void OnClosing(CancelEventArgs args) { try { Di rectory.CreateDi rectory( Path.GetDi rectoryName(strFi1 eName)): Fi1e.Wri teAl1 Text(strFi1 eName, txtbox.Text); } catch (Exception exc) { MessageBoxResult result = MessageBox.ShowC'File could not be saved; " + exc.Message + "\nClose program anyway?", Title, MessageBoxButton.YesNo, MessageBoxImage.Exclamati on); args.Cancel = (result == MessageBoxResult.No): } void TextBoxOnKeyDown(object sender. KeyEventArgs args) { if (args.Key == Key.F5) { txtbox. Sei ectedText = DateTime.Now.ToStringO:
92 Глава 4. Кнопки и другие элементы управления txtbox.Caretindex = txtbox.Selectionstart + txtbox.Seiect1onLength; } } } } Программа определяет местонахождение и имя файла в области, называемой локальным изолированным хранилищем. Статический метод File.ReadAIIText пы- тается загрузить файл в конструкторе окна, а статический метод File.WriteAIIText сохраняет файл. Программа также устанавливает обработчик события KeyDown, чтобы при нажатии клавиши F5 в файл вставлялась текущая дата и время. Объект RichTextBox намного сложнее TextBox, потому что хранящийся в нем текст может быть оформлен разным форматированием абзацев и символов. Если вы знакомы с предшествующими аналогами RichTextBox в системе Windows, мо- жет возникнуть естественное предположение, что текст хранится в формате RTF (Rich Text Format) — формате, появившемся в середине 1980-х годов и пред- назначенном для передачи форматированного текста между программами оформ- ления текста. Конечно, WPF-версия поддерживает RichTextBox (и простой текст), но она также поддерживает и формат на базе XML, являющийся подмноже- ством языка XAML (Extensible Application Markup Language), поддерживае- мого WPF. Следующая программа использует стандартные диалоговые окна открытия/ сохранения файлов из пространства имен Microsoft.Win32. Программа не имеет меню, поэтому диалоговые окна вызываются клавишами Ctrl+O и Ctrl+S. Эта осо- бенность создает проблему: поле RichTextBox перехватывает все нажатия клавиш. Если присоединить обработчик к событию KeyDown элемента, комбинации откры- тия окон будут потеряны. Проблема решается методом, который более подробно рассматривается в гла- ве 9. События клавиатуры, мыши и стилуса сначала происходят в окне в форме предварительных событий. Следовательно, чтобы проанализировать нажатия клавиш до того, как они будут получены RichTextBox, следующая программа пе- реопределяет метод OnPreviewKeyDown окна. Обнаружив комбинации Ctrl+O или Ctrl+S, переопределение вызывает соответствующее диалоговое окно и задает свой- ству Handled аргументов события истинное значение; это означает, что событие уже было обработано и нажатие клавиш не нужно передавать RichTextBox. Так как в программе отсутствуют меню и панели инструментов, возмож- ности форматирования текста ограничиваются встроенными комбинациями кла- виш Ctrl+I, Ctrl+U и Ctrl+B для применения курсива, подчеркивания и жирного начертания. EditSomeRichText.cs // // EditSomeRichText.cs (с) 2006 by Charles Petzold //---'- using Microsoft.Win32;
Кнопки и другие элементы управления 93 using System; using System.10; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; namespace Petzol d.Edi tSomeRi chText public class EditSomeRichText ; Window { RichTextBox txtbox; string strFilter = "Document Files(*.xaml)|*.xaml|A11 files (*.*)|*.*"; [STAThread] public static void MainO { Application app = new Application); app.Run(new EditSomeRichTextO) • } public EditSomeRichTextO { Title = "Edit Some Rich Text"; txtbox = new RichTextBoxO; txtbox.VerticalScrollBarVisibility = ScrollBarVisi bi 1ity.Auto; Content = txtbox; txtbox.Focus О; } protected override void OnPreviewTextInput( TextCompositionEventArgs args) if (args.ControlText.Length > 0 && args.ControlText[0] == '\xOF') { OpenFileDialog dig = new OpenFileDialog(); dlg.CheckFileExists = true; dig.Filter = strFilter; if ((bool)dlg.ShowDialog(this)) { FlowDocument flow = txtbox.Document; TextRange range = new TextRange(flow.Contentstart. flow.ContentEnd): Stream strm = null; try {
94 Глава 4. Кнопки и другие элементы управления strm = new Fl 1eStream(dlg.Fi1 eName. FileMode.Open); range.Load(strm, DataFormats.Xaml); } catch (Exception exc) { MessageBox.Show(exc.Message, Title): } finally { If (strm != null) strm. Closed; } } args.Handled = true; } if (args.Control Text.Length > 0 && args.ControlText[0] == '\xl3‘) { SaveFileDialog dig = new SaveFIleDialogO; dig.Filter = strFilter; if ((bool)dlg.ShowDialog(this)) { FlowDocument flow = txtbox.Document; TextRange range = new TextRange(flow.Contentstart, flow.ContentEnd); Stream strm = null; try strm = new FileStream(dlg.FileName, FileMode.Create); range.Save(strm. DataFormats.Xaml); } catch (Exception exc) { MessageBox.Show(exc.Message, Title); } finally { if (strm != null) strm.Close(); } } args.Handled = true;
Кнопки и другие элементы управления 95 base.OnPrevIewTextInput(a rgs); } } } Отформатированный текст читается и записывается в RichTextBox через свой- ство Document. Свойство относится к типу FlowDocument, а следующий код созда- ет объект TextRange, включающий весь документ: TextRange range = new TextRange(flow.Contentstart. flow.ContentEnd); Класс TextRange определяет два метода с именами Load и Save. В первом аргу- менте передается объект Stream, а во втором — строка с описанием требуемого формата данных. Значение второго аргумента проще всего получить из статиче- ских полей класса DataFormats. С объектом RichTextBox можно использовать фор- маты DataFormats.Text, DataFormats.Rtf, DataFormats.Xaml и DataFormats.XamIPackage (ZIP-файл с двоичными ресурсами, которые могут потребоваться документу). Программа жестко запрограммирована на использование DataFormats.Xaml (впро- чем, она не сможет загрузить произвольный файл XAML). Вероятно, мы могли бы написать еще множество полезных и замечательных программ, окно которых содержит единственный объект, но мне ничего в голо- ву не приходит. А значит, мы должны научиться размещать в окне несколько элементов.
g StackPanel и WrapPanel Элементы, производные от класса Contentcontrol (такие, как Window, Button, Label и ToolTip), содержат свойство Content, которому можно присвоить практически любой объект. Обычно таким объектом является строка или экземпляр класса, производного от UIEIement. Однако при этом возникает одна проблема: свойству Content задается только один объект; этого достаточно для простой кнопки, но не для окна. К счастью, WPF содержит несколько классов, предназначенных специаль- но для решения этой проблемы. Они объединяются общим термином «панели», а процесс размещения элементов управления и других интерфейсных элементов на панели называется макетированием. Классы панелей являются производными от класса Panel. В следующем фраг- менте иерархии классов представлены важнейшие потомки Panel: UIEIement FrameworkElement Panel (абстрактный) Canvas DockPanel Grid StackPanel Uni formGrid WrapPanel Концепция панели появилась в графических оконных средах относительно недавно. Традиционно при размещении элементов в окнах Windows-программы точно указывали их точные размеры и местоположение. С другой стороны, WPF в основном ориентируется на динамическое макетирование (также называемое автоматическим макетированием}. Сами панели выбирают размеры и позиции элементов на основании различных моделей макетирования. По этой причине Panel имеет столько производных классов: каждый класс поддерживает свой тип макетирования. Класс Panel определяет свойство Children, используемое для хранения дочерних элементов. Свойство Children представляет собой объект типа UIEIementCollection, то есть коллекцию объектов UIEIement. Другими словами, дочерними элементами
StackPanel и WrapPanel 97 панели могут быть объекты Image, объекты Shape, объекты TextBlock, объекты Control и т. д. — я назвал лишь самых популярных кандидатов. Кроме того, среди дочерних элементов панели могут быть другие панели. Подобно тому, как па- нель используется для группировки нескольких интерфейсных элементов окна, она также может группировать элементы внутри кнопки или любого другого объекта Contentcontrol. В этой главе рассматривается класс StackPanel, объединяющий дочерние эле- менты в вертикальный или горизонтальный стек, и класс WrapPanel, сходный со StackPanel (но обеспечивающий возможность переноса дочерних элементов .в следующий столбец или строку). Следующая глава посвящена классу DockPanel, автоматизирующему стыков- ку (размещение элементов у внутренних границ своих родителей), и классу Grid, выстраивающему дочерние элементы в виде сетки из нескольких строк и столб- цов. Класс UniformGrid является аналогом Grid, но все строки в нем имеют одина- ковую высоту, а все столбцы — одинаковую ширину. Затем наступает время для знакомства с классом Canvas, позволяющим упо- рядочивать элементы с указанием точных координат. Конечно, панель Canvas близка к традиционному механизму макетирования и, вероятно, используется реже всех из пяти перечисленных вариантов. Автоматическое макетирование принадлежит к числу важнейших особенно- стей WPF, однако при его использовании необходима осторожность. Как обыч- но, при настройке некоторых свойств элементов (прежде всего HorizontalAligment, VerticalAlignment, Margin и Padding) следует руководствоваться собственными эсте- тическими чувствами. Следующая программа задает свойству Content окна объект StackPanel, а затем создает 10 кнопок, которые являются дочерними элементами панели. StackTenButtons.cs // // StackTenButtons.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzol d.StackTenButtons { class StackTenButtons : Window { [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new StackTenButtonsO);
98 Глава 5. StackPanel и WrapPanel } public StackTenButtonsO { Title = "Stack Ten Buttons"; StackPanel stack = new StackPanel(): Content = stack; Random rand = new Random!); for (int i = 0; 1 < 10: i++) { Button btn = new ButtonO: btn.Name = ((char)UA' + i)).ToString(): btn.FontSize += rand.Next(10); btn.Content = "Button " + btn.Name + " says 'Click me'"; btn.Click += ButtonOnClick; stack.Children.Add(btn): } } void ButtonOnClick(object sender, RoutedEventArgs args) { Button btn = args.Source as Button; MessageBox.Show("Button " + btn.Name + " has been clicked", "Button Click"); } } } Пограмма создает объект типа Random, а затем увеличивает свойство FontSize каждой кнопки на небольшую случайную величину. Кнопки включаются в кол- лекцию Children объекта StackPanel следующей командой: stack.Children.Add(btn): Разные размеры шрифтов кнопок демонстрируют, как StackPanel работает с интерфейсными элементами разных размеров. При запуске программы кноп- ки выстраиваются сверху вниз в порядке их включения в коллекцию Children. Каждая кнопка обладает высотой, достаточной для вывода ее содержимого, од- нако ширина каждой кнопки выбирается по ширине StackPanel, которая, в свою очередь, заполняет клиентскую область окна. Поэкспериментировав с программой, попробуйте назначить StackPanel нестан- дартную кисть закраски фона, чтобы лучше видеть размеры панели: stack.Background = Brushes.Aquamarine: По умолчанию StackPanel размещает свои дочерние элементы по вертикали. Данное поведение может быть изменено при помощи свойства Orientation: stack.Orientation = Orientation.Horizontal: Теперь ширина кнопок выбирается по ширине содержимого, зато по вертика- ли кнопки распространяются на всю высоту панели. В зависимости от горизон-
сыгкРапб! и WrapPanel 99 тальных размеров экрана можно заметить, что кнопки, не помещающиеся на эк- ране, безжалостно усекаются. и В завершающий части эксперимента мы вернемся к вертикальной ориентации панели. Однако следует помнить, что последующее описание относится к обоим вариантам ориентации (конечно, вертикали нужно будет заменить на горизон- тали, и наоборот). Вряд ли вы действительно хотите, чтобы кнопки простирались на всю шири- ну окна. Чтобы сократить их ширину, задайте свойство HorizontalAlignment каж- дой кнопки в цикле for. btn.Horizontal Alignment = HorizontalAlignment.Center: Объект StackPanel, как и прежде, занимает всю ширину клиентской области, но кнопки теперь имеют нормальные размеры. Также возможно другое реше- ние — задание свойства HorizontalAlignment для самого объекта StackPanel: stack.HorizontalAlignment = HorizontalAlignment.Center: Теперь размеры StackPanel велики лишь настолько, чтобы вместить макси- мальную ширину кнопок. Цвет фона StackPanel демонстрирует небольшое раз- личие между горизонтальным выравниванием кнопок и панели: если задать свойство HorizontalAlignment каждой кнопки равным Center, то ширина каждой кнопки подбирается по ее содержимому. Если же задать значение Center свойст- ву HorizontalAlignment объекта StackPanel, то панель становится достаточно широ- кой для размещения самой широкой кнопки. При этом каждая кнопка по-преж- нему обладает свойством HorizontalAlignment=Stretch, поэтому она автоматически расширяется по ширине StackPanel. В результате все кнопки имеют одинаковую ширину. Возможно, самым удовлетворительным решением был бы отказ от задания свойств HorizontalAlignment как для панели, так и для кнопок, и простая подгонка размеров окна по содержимому: SizeToContent = SizeToContent.WidthAndHeight: ResizeMode = ResizeMode.CanMinimize: Теперь размеры окна в точности соответствуют размерам содержимого, то есть размера StackPanel, отражающего ширину самой широкой кнопки. Возможно, вы предпочтете, чтобы все кнопки имели одинаковую ширину, или чтобы ширина каждой кнопки соответствовала ее конкретному содержимому; это сугубо эсте- тический выбор, который реализуется заданием свойства HorizontalAlignment для каждой кнопки. Впрочем, что вам заведомо не понравится, так это кнопки, сбившиеся вплот- ную друг к другу, как в рассмотренных примерах. Определите для каждой кноп- ки внешние отступы: btn.Margin = new Thlckness(5): Так получается намного лучше — каждая кнопка снабжается внешними отсту- пами шириной в 5 аппаратно-независимых единиц (около 1/20 дюйма). Впрочем, некоторым педантам и это решение покажется не вполне удовлетворительным.
100 Глава 5. StackPanel и WrapPanel Поскольку каждая кнопка окружена отступами в 1/20 дюйма по всему перимет- ру, расстояние между двумя соседними кнопками составит 1/10 дюйма, тогда как расстояние между кнопками и границей окна равно всего 1/20 дюйма. Про- блема решается назначением отступов для самой панели: stack.Margin = new Thickness(5): Остается удалить фоновую кисть для StackPanel, и мы получаем вполне сим- патичную (хотя и ничего не делающую) программу. Возможно, вы заметили, что при задании свойства Context объектов Button сначала задают свойству Name кнопки короткую строку — "А", "В", "С" и т. д. В дальнейшем эти строки могут использоваться для обращения к объектам, с которыми они связаны, — вызовите метод FindName, определяемый в классе FrameworkElement. Например, обработчик события или другой метод класса окна может содержать следующий код: Button btn = FindNameCE") as Button: Хотя вызывается класс FindName объекта Window, метод проводит рекурсив- ный поиск по содержимому окна и StackPanel. Также поддерживается индексирование по свойству Children панели. Напри- мер, следующее выражение возвращает шестой элемент, включенный в коллек- цию Children: stack.Children[5] Класс UIEIementCollection содержит ряд методов, упрощающих работу с дочер- ними элементами. Скажем, для элемента коллекции el выражение stack.Children.IndexOf(el) возвращает индекс элемента в коллекции, или -1, если элемент не является ча- стью коллекции. Класс UIEIementCollection также содержит методы вставки эле- мента в позицию с заданным индексом и удаления элементов из коллекции. В обработчиках событий — таких, как ButtonOnClick в программе StackTen- Buttons — часто возникает задача получения объекта, сгенерировавшего событие. До настоящего момента я пользовался традиционным методом .NET: первый ар- гумент обработчика (обычно sender) преобразуется в объект нужного типа: Button btn = sender as Button: Однако программа StackTenButtons игнорирует аргумент sender и использу- ет вместо него свойство объекта RoutedEventArgs, который является вторым аргу- ментом обработчика события: Button btn = args.Source as Button: В этой конкретной программе выбор метода не столь важен. Источник собы- тия Click, полученный из свойства Source объекта RoutedEventArgs, совпадает с объ- ектом, определяемым из аргумента sender. Если обработчик события присоеди- нен к элементу, породившему событие, эти два значения совпадают.
StackPanel и WrapPanel 101 Но в другом сценарии обработки событий эти два значения могут разли- чаться. Чтобы опробовать этот сценарий, закомментируйте команду цикла for, назначающую обработчик события Click каждой кнопки, а затем включите сле- дующую команду в конец конструктора за пределами цикла for: stack.AddHandler(Button. Cl ickEvent, new RoutedEventHandler(ButtonOnClick)); Метод AddHandler определяется классом UIEIement. Его первым аргументом должен быть объект типа RoutedEvent. ClickEvent — статическое поле объекта RoutedEvent, доступное только для чтения, которое определяется классом Button- Base и наследуется Button. Второй аргумент определяет обработчик события, связываемый с событием. Он должен быть задан в форме конструктора делегата обработчика события, в данном случае RoutedEventHandler. Вызов AddHandler приказывает StackPanel отслеживать во всех дочерних эле- ментах события типа Button.ClickEvent и использовать ButtonOnClick в качестве об- работчика этих событий. После перекомпиляции программа работает точно так же, но через обработчик события поступает несколько иная информация. Свой- ство Source объекта RoutedEventArgs по-прежнему идентифицирует объект Button, но в аргументе sender теперь передается объект StackPanel. Вызов AddHandler не обязан относиться к объекту StackPanel. С таким же успе- хом можно вызвать AddHandler для самого окна: AddHandler(Button.Cl 1ckEvent, new RoutedEventHandler(ButtonOnCl1ck)); Теперь один обработчик события будет относиться к объектам Button в любых позициях иерархического дерева, корнем которого является объект окна. Аргу- мент sender обработчика событий при этом определяет объект Window. Мы еще вернемся к теме маршрутов обработки событий в главе 9. Допускается создание вложенных панелей. Следующая программа отобража- ет 30 кнопок в три столбца по 10 кнопок каждый. В ней задействованы четыре объекта StackPanel. StackThirtyButtons.cs // // StackThirtyButtons.cs (с) 2006 by Charles Petzold // using System; using System.Windows: using System.Windows.Controls: using System.Windows.Input; using System.Windows.Media: namespace Petzol d.StackThi rtyButtons { class StackThirtyButtons : Window { [STAThread] public static void MainO
102 Глава 5. StackPanel и WrapPanel { Application app = new ApplicationO; app.Run(new StackThi rtyButtons()); } public StackThirtyButtons() Title = "Stack Thirty Buttons"; SizeToContent = SizeToContent.WidthAndHeight; ResizeMode = ResizeMode.CanMinimize: AddHandler(Button.Cli ckEvent, new RoutedEventHandler(ButtonOnClick)); StackPanel stackMain = new StackPanel0; stackMain.Orientation = Orientation.Horizontal; stackMain.Margin = new Thickness(5); Content = stackMain: for (int i = 0; i < 3; i++) { StackPanel stackChild = new StackPanel0; stackMain.Chi 1dren.Add(stackChi Id); for (int j = 0; j < 10; j++) Button btn = new ButtonO: btn.Content = "Button No. '' + (10 * i + j + 1); btn.Margin = new Thickness(5); stackChild.Children.Add(btn): } } void Button0nC11ck(object sender. RoutedEventArgs args) { MessageBox.ShowC’You clicked the button labeled " + (args.Source as Button).Content): } } } Обратите внимание на вызов AddHandler в начале конструктора. Все кнопки совместно используют один обработчик события Click. Конструктор создает один объект StackPanel с горизонтальной ориентацией, заполняющий клиентскую об- ласть. Этот объект StackPanel является родителем для трех других объектов Stack- Panel, каждый из которых ориентирован вертикально, и является родителем для 10 кнопок. Кнопкам и первому объекту StackPanel назначаются внешние отступы в 5 аппаратно-независимых единиц, а размеры окна подгоняются по результату. Рано или поздно добавляемые кнопки перестают помещаться на экране, и для их просмотра возникает необходимость в прокрутке. Однако вместо полосы про- крутки в данном случае лучше воспользоваться классом Scrollviewer. Как и Window с ButtonBase, класс Scrollviewer является производным от Contentcontrol. Различие
StackPanel и WrapPanel 103 состоит в том, что если содержимое Scrollviewer не помещается в элементе, Scroll- Viewer автоматически обеспечивает его прокрутку. В следующей программе свойству Content окна задается объект типа Scroll- Viewer, а свойству Content объекта Scrollviewer — объект StackPanel с 50 кнопками. ScrollFiftyButtons.cs //---------------------------------------------------- // ScrollFiftyButtons.cs (с) 2006 by Charles Petzold // using System: using System.Windows: using System.Windows.Controls; using System.Windows.Input: using System.Windows.Media: namespace Petzold.Scrol1 FlftyButtons { class Scroll FlftyButtons : Window { [STAThread] public static void MalnO ( Application app = new ApplicationO: app.Run(new ScrollFiftyButtonsО): } public Scrol1 FlftyButtons() { Title = "Scroll Fifty Buttons": SizeToContent = SizeToContent.Width: AddHandler(Button.Cli ckEvent, new RoutedEventHandler(ButtonOnClick)); Scroll Viewer scroll = new Scroll Viewer!); Content = scroll: StackPanel stack = new StackPanel(); stack.Margin = new Thickness(5); scroll.Content = stack: for (int 1=0: i < 50; 1++) { Button btn = new Button!); btn.Name = "Button" + (i + 1): btn.Content = btn.Name + " says 'Click me'": btn.Margin = new Thickness(5); stack. Children.Add(btn); } } void ButtonOnClick(object sender, RoutedEventArgs args)
104 Глава 5. StackPanel и WrapPanel Button btn = args.Source as Button: if (btn != null) MessageBox.Show(btn.Name + " has been clicked", "Button Click"): } } } Если размеры окна (или экрана) недостаточно велики для отображения всех 50 кнопок, для получения доступа к нижним кнопкам можно воспользоваться полосой прокрутки. Класс Scrollviewer обладает двумя свойствами, управляющими видимостью вер- тикальной и горизонтальной полос прокрутки. По умолчанию свойство Vertical- ScrollBarVisibility равно ScrollBarVisibility.Visible; это означает, что полоса прокрутки отображается всегда, но не активна тогда, когда она не используется. Свойство HorizontalScrollBarVisibility по умолчанию равно ScrollBarVisibility.Disabled; это значит, что полоса прокрутки не отображается никогда. При другом значении — ScrollBarVisibility.Auto — полоса прокрутки отображается только в случае необходимости. Попробуйте задать его свойству VerticalScrollBar и сократить количество кнопок до значения, которое бы помещалось на экране. Обратите внимание: если сделать окно достаточно высоким, чтобы полоса про- крутки исчезла, все кнопки становятся шире, поскольку у них появляется до- полнительное место для растяжения! (Конечно, этого эффекта можно избежать, задав свойству HorizontalAlignment значение HorizontalAlignment.Center.) Свойству HorizontalScrollBarVisibility можно задать значение ScrollBarVisibility.Visible или ScrollBarVisibility.Auto. Если теперь окно станет слишком узким для кнопок, вы сможете просмотреть содержимое правой части панели при помощи гори- зонтальной полосы прокрутки. Без горизонтальной полосы прокрутки сужение окна приведет к сужению StackPanel. Ширина кнопок сокращается, а их содер- жимое усекается. Горизонтальная полоса прокрутки обеспечивает нужную ши- рину StackPanel. В программе каждой кнопке назначаются внешние отступы в 5 единиц с ка- ждой стороны; такие же отступы задаются для StackPanel. С другой стороны, для объекта Scrollviewer свойство Margin не задается, потому что полоса прокрутки, отстоящая от краев окна, выглядит очень странно. Ближе к началу конструктора свойство SizeToContent задается так, чтобы толь- ко ширина окна изменялась по его содержимому: SizeToContent = SizeToContent.Width: При обычном значении SizeToContent.WidthAndHeight размеры окна выбирают- ся так, чтобы в нем отображались все 50 кнопок, а это довольно бессмысленно. Более того, конструктор не задает свойство ResizeMode=ResizeMode.CanMinimize, как ранее часто делалось для окон, размеры которых выбирались по размерам содер- жимого. Дело в том, что этот режим блокирует возможность изменения разме- ров окна и не позволяет экспериментировать с его шириной и высотой.
StackPanel и WrapPanel 105 Также обратите внимание на небольшие изменения в обработчике события Click. После преобразования свойства Source объекта RoutedEventArgs в объект Button обработчик проверяет, отличен ли результат от null. Результат равен null, если Source не является объектом Button. Как такое возможно? Попробуйте сами: за- комментируйте команду if, перекомпилируйте программу и щелкните на стрелке в конце полосы прокрутки. Программа аварийно завершается с выдачей исклю- чения null-ссылки. Если разобраться в происходящем, выясняется, что проблема возникает, ко- гда свойство Source объекта RoutedEventArgs ссылается на объект ScrolLBar. Объ- ект типа ScrollBar не может быть преобразован в объект типа Button. Но согласно документации, класс ScrollBar даже не реализует событие Click. Почему же объект типа ScrollBar оказывается в обработчике события? Чтобы найти ответ, достаточно проверить другое свойство RoutedEventArgs с именем OriginalSource. Некоторые элементы управления состоят из других эле- ментов, и ScrollBar служит хорошим примером. Две стрелки на концах полосы представляют собой объекты RepeatButton, генерирующие события Click. В свой- стве OriginalSource объекта RoutedEventArgs указан объект типа RepeatButton. Помните об этом при назначении обработчиков событий родительского объ- екта. Возможно, вам придется включить в обработчик дополнительную провер- ку. В этой конкретной программе проблема также решается вызовом AddHandler для объекта StackPanel (вместо окна). Традиционно для просмотра содержимого областей, не помещающихся в окне, использовались полосы прокрутки. WPF предлагает другое решение — класс ViewBox. Чтобы опробовать его, сначала закомментируйте в ScrollFiftyButtons три строки кода, относящихся к Scrollviewer. Создайте объект типа ViewBox и задайте его свойству Child объект StackPanel: Viewbox view = new ViewboxO; Content = view; view.Child = stack: Теперь весь объект StackPanel с 50 кнопками уменьшаются в размерах так, чтобы помещаться в окне. Конечно, это не лучшее решение для кнопок с тек- стом, и все же помните о нем, когда у вас возникнут проблемы со свободным местом на экране. Пока мы рассматривали проблему размещения нескольких кнопок в окне. Объ- ект StackPanel также может использоваться для размещения нескольких элемен- тов в кнопках. Следующая программа создает одну кнопку и размещает в ней StackPanel с четырьмя дочерними элементами: Image, Label и два объекта Polyline (класс, производный от Shape). DesignAButton.cs //------------------------------------------- // DesignAButton.cs (с) 2006 by Charles Petzold //------------------------------------------- using System;
106 Глава 5. StackPanel и WrapPanel using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; namespace Petzold.DesignAButton { public class DesignAButton : Window { [STAThread] public static void MainO { Application app = new ApplicationO; app. Run (new DesignAButtonO); } public DesignAButtonO { Title = "Design a Button"; // Создание объекта Button как содержимого окна Button btn = new Button О; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.Vertical Alignment = VerticalAlignment.Center; btn.Click += ButtonOnClick; Content = btn; // Создание объекта StackPanel как содержимого Button. StackPanel stack = new StackPanel 0; btn.Content = stack; // Добавление объекта Polyline к StackPanel stack.Children.Add(ZigZag(10)); // Добавление объекта Image Uri uri = new Uri("pack://application:,,/ВООКОб.ICO"); Bitmapimage bitmap = new Bitmaplmage(uri); Image img = new ImageO; img.Margin = new Thickness(O, 10, 0, 0); img.Source = bitmap; img.Stretch = Stretch.None; stack.Children.Add(img); // Добавление объекта Label Label Ibl = new Label(); 1 bl.Content = "_Read books!"; Ibl.HorizontalContentAlignment = HorizontalAlignment.Center;
StackPanel и WrapPanel 107 stack. Children. Add (IN); // Добавление объекта Polyline stack.Chi 1dren.Add(Z1gZag(0)): } Polyline ZlgZagdnt offset) { Polyline poly = new PolyllneO; poly.Stroke = SystemColors.ControlTextBrush; poly.Points = new PolntCollectlonO: for (int x = 0; x <= 100; x += 10) poly.Points.Add(new Po1nt(x, (x + offset) % 20)): return poly: } void ButtonOnClick(object sender, RoutedEventArgs args) { MessageBox.Show("The button has been clicked". Title): } } Два объекта Polyline создаются методом ZigZag. Они выглядят одинаково (зиг- загообразная линия), если не считать того, что второй объект инвертирован. Элемент Image содержит маленькое изображение книги, найденное мной в коллек- ции значков и растровых изображений из поставки Visual Studio 2005. Я снаб- дил рисунок внешним отступом в 10 единиц, но только у верхнего края, чтобы немного отделить его от Polyline. Элемент Label содержит текст «Read books!»; обратите внимание на символ подчеркивания перед буквой R. Хотя этот элемент Label является одним из че- тырех элементов на кнопке (причем даже не первым), подчеркивания в его тек- стовом содержимом достаточно, чтобы для ускоренного вызова кнопки могла применяться комбинация клавиш Alt+R. Конечно, вам вряд ли когда-либо придется создавать группы из 50, 30 или хотя бы 10 кнопок, и все же один тип кнопок почти всегда существует в груп- пах. Речь идет о переключателях (RadioButton). Традиционно несколько взаимоисключающих переключателей создаются как дочерние элементы группового поля — элемента управления, который пред- ставляет собой простую рамку с текстовым заголовком. В WPF групповые поля представлены классом GroupBox — одним из трех классов, производных от Head- eredContentControl: Control Contentcontrol HeaderedContentControl Expander GroupBox Tabitem
108 Глава 5. StackPanel и WrapPanel Так как класс HeaderedContentControl является производным от Contentcontrol, все перечисленные элементы обладают свойством Content. У них также имеется свойство Header, которое (как и Content) относится к типу object. Класс GroupBox не добавляет к HeaderedContentControl никаких новых членов. Свойство Header используется для задания заголовка в верхней части GroupBox. Обычно в заголовке выводится текстовая строка, но теоретически это может быть любой объект. Свойство Content определяет внутреннее содержимое GroupBox. Ве- роятно, для группы переключателей ему будет уместно задать объект StackPanel. Приложение TuneTheRadio показывает, как создать группу из четырех пере- ключателей с использованием объекта StackPanel. Переключатели динамически изменяют свойство Windowstyle окна. TuneTheRadio.cs // // TuneTheRadio.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.TuneTheRadio { public class TuneTheRadio : Window { [STAThread] public static void MainQ { Application app = new Application): app.Run(new TuneTheRadioO): } public TuneTheRadioO { Title = "Tune the Radio"; SizeToContent = SizeToContent.WldthAndHelght; GroupBox group = new GroupBox(); group.Header = "Window Style"; group.Margin = new Th1ckness(96); group.Padding = new Th1ckness(5); Content = group; StackPanel stack = new StackPanelО; group.Content = stack; stack.Children.Add( CreateRadloButtonCNo border or caption", WlndowStyle.None));
StackPanel и WrapPanel 109 stack.Children.Add( CreateRadioButton("Single-border window", WindowSty1e.Si ngleBorderWi ndow)); stack.Children.Add( CreateRadioButton("3D-border window", Wi ndowSty1e.ThreeDBorderWi ndow)); stack.Children.Add( CreateRadi oButton("Tool wi ndow", WindowStyle.Tool Window)); AddHandler(RadioButton.CheckedEvent, new RoutedEventHandler(RadioOnChecked)); } RadioButton CreateRadioButton(string strText, WindowStyle winstyle) { RadioButton radio = new RadioButton(); radio.Content = strText; radio.Tag = winstyle; radio.Margin = new Thickness(5); radio.IsChecked = (winstyle == WindowStyle); return radio; } void RadioOnChecked(object sender, RoutedEventArgs args) { RadioButton radio = args.Source as RadioButton; WindowStyle = (WindowStyle)radio.Tag; } } } Объекту GroupBox назначены внешние отступы размером в один дюйм, чтобы он лучше выделялся на фоне окна. Кроме того, ему назначены внутренние отступы в 1/20 дюйма — их ширина совпадает с шириной внешних отступов каждого пере- ключателя (также можно задать свойству Margin внутреннего объекта StackPanel то же значение). Переключатели отделяются друг от друга и от внутренней гра- ницы GroupBox промежутком в 1/10 дюйма. Метод CreateRadioButton задает свой- ству Content каждой кнопки текстовую строку и задает свойству Тад соответст- вующее значение из перечисления WindowStyle. Кроме того, CreateRadioButton устанавливает флаг IsChecked переключателя, соответствующего текущему со- стоянию свойства WindowStyle окна. Установка и снятие переключателей группы происходят автоматически, при- чем пользователь может работать как с мышью, так и с клавиатурой. При работе с клавиатурой клавиши со стрелками передают фокус ввода между переключа- телями, а клавиша «пробел» устанавливает текущий переключатель, обладающий фокусом. Программе остается только следить за тем, какой переключатель уста- новлен в данный момент времени. (Некоторые программы также должны полу- чать информацию о снятии переключателей.) Обычно удобнее всего использовать один обработчик события для всех переключателей группы. При написании кода обработчика следует помнить, что свойство IsChecked уже задано.
110 Глава 5. StackPanel и WrapPanel Может оказаться, что набор переключателей одного уровня состоит из двух и более взаимоисключающих групп. Чтобы различать эти группы, восполь- зуйтесь свойством GroupName, определяемым в классе RadioButton. Задайте ему уникальную строку для переключателей каждой взаимоисключающей группы, и переключатели в разных группах будут автоматически устанавливаться и сни- маться. Панель WrapPanel имеет много общего с StackPanel. Более того, на стадии про- ектирования и создания WPF класс WrapPanel создавался как предшественник StackPanel. Содержимое WrapPanel выводится по строкам или столбцам, причем при нехватке места элементы автоматически переносятся в следующую строку (или столбец). Казалось бы, удобно... но потом выяснилось, что на практике разработчики чаще всего использовали WrapPanel без переноса. Оказывается, им был больше нужен класс StackPanel. Класс WrapPanel пригодится при отображении неизвестного количества эле- ментов в двумерной области (например, как в Проводнике Windows в режимах значков). Возможно, все отображаемые элементы имеют одинаковые размеры. Класс WrapPanel не выдвигает такого требования, но содержит свойства ItemHeight и ItemWidth для задания единой ширины и высоты элементов. Кроме этих свойств, WrapPanel определяет только свойство Orientation, которое используется так же, как и для StackPanel. Трудно представить применение WrapPanel, в котором не был бы задействован класс Scrollviewer. Давайте при помощи классов WrapPanel и Scrollviewer, а также нескольких кнопок создадим простейшую имитацию правой панели Проводни- ка Windows. Программа называется ExploreDirectories и состоит из двух клас- сов. Вот первый из них. ExploreDirectories.cs // // ExploreDirectories.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ExploreDirectories { public class ExploreDirectories : Window { [STAThread] public static void MainO { Application app = new Application); app.Run(new ExploreDirectories()):
StackPanel и WrapPanel 111 public ExploreDirectories() { Title = "Explore Directories": Scrollviewer scroll = new ScrollVIewerO: Content = scrol1: WrapPanel wrap = new WrapPanel 0; scroll.Content = wrap; wrap.Children.Add(new Fl 1eSystemlnfoButton()): } } } Видите, как просто? Содержимым окна назначается объект Scrollviewer, а со- держимым Scrollviewer — объект WrapPanel, имеющий один дочерний элемент — объект типа FileSystemlnfoButton. Я назвал класс FileSystemlnfoButton по имени объекта FileSystemlnfo из про- странства имен System.IO. FileSystemlnfo имеет два производных класса: Fileinfo и Directoryinfo. Оператор is проверяет, к чему относится конкретный объект типа FileSystemlnfo — к файлу или каталогу. Класс FileSystemlnfoButton, производный от Button, хранит объект FileSystemlnfo в виде поля. Он имеет три конструктора. Чаще всего применяется конструктор с одним аргументом типа FileSystemlnfo, содержимое которого сохраняется в поле. Конструктор задает свойству Content кнопки свойство Name указанного объек- та. Значение свойства определяет имя каталога или имя файла. Для объектов Directoryinfo текст выделяется жирным шрифтом. Непараметризованный конструктор используется только при запуске про- граммы и добавлении первого дочернего элемента в объект WrapPanel. Конструк- тор передает объект Directoryinfo для каталога Мои документы конструктору с од- ним аргументом. FileSystemlnfoButton.cs //------------------------------------------------------ // FileSystemlnfoButton.cs (с) 2006 by Charles Petzold //------------------------------------------------------ using System: using System.Diagnostics: // Для класса Process using System.10: using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: namespace Petzold.ExploreDi rectori es
112 Глава 5. StackPanel и WrapPanel public class FileSystemlnfoButton ; Button { FileSysteminfo Info; // ^параметризованный конструктор создает кнопку // для каталога "Мои документы" public FIleSystemlnfoButtonO this(new DI rectoryInfо( Envlronment.GetFolderPath( Envlronment.Sped al Folder.MyDocuments))) { // Конструктор с одним аргументом создает кнопку // для каталога или файла public F11eSystemInfoButton(F11eSystemInfo info) { this.Info = Info; Content = Info.Name; If (Info Is DI rectoryInfo) FontWeight = FontWelghts.Bold; Margin = new Thickness(10); // Конструктор с двумя аргументами создает кнопку // родительского каталога public F11eSystemInfoButton(F11eSystemInfo Info, string str) thls(lnfo) { Content = str; } // Переопределение OnCUck делает все остальное protected override void OnClickO { if (info is Fileinfo) { Process.Start!info.Full Name); } else if (info is Di rectoryInfo) { DI rectoryInfo dlr = Info as DI rectoryInfo; Appllcation.Current.MainWindow.Title = dir.Full Name; Panel pnl = Parent as Panel; pnl.Children.ClearO; if (dir.Parent != null) pnl.Children.Add(new FileSystemInfoButton( dir.Parent, ".."));
StackPanel и WrapPanel 113 foreach (FileSystemInfo inf in dir.GetFileSystemlnfosO) pnl.Children.Add(new FileSystemlnfoButton(inf)); } base.OnClickO ; } } } При запуске программы вы увидите одну кнопку с надписью Му Documents. Щелчок на кнопке показывает содержимое каталога. Если щелкнуть на кнопке с именем файла, запускается приложение, связанное с данным типом файлов. В большинстве каталогов также присутствует кнопка с двумя точками (..), при помощи которой можно вернуться к родительскому каталогу. Если такая кноп- ка не отображается, значит, вы находитесь в корневом каталоге диска, и даль- нейший переход невозможен. Вся эта логика реализована в обработчике OnClick. Обработчик сначала про- веряет, является ли объект info, хранимый в поле, объектом типа Fileinfo. Если условие выполняется, он использует статический метод Process.Start (из про- странства имен System.Diagnostics) для запуска приложения. Если объект info представляет каталог, то новое имя каталога отображает- ся в заголовке окна. Далее метод OnClick удаляет текущие дочерние элементы с WrapPanel. Если новый каталог не является корневым, то конструктор с двумя аргументами создает кнопку перехода на верхний уровень (..). Работа OnClick за- вершается получением содержимого каталога от GetFileSystemlnfos и созданием объектов FileSystemlnfoButton для всех полученных элементов. Обязательно откройте большой каталог, содержащий много файлов, и посмот- рите, как Scrollviewer открывает доступ к скрытой части содержимого WrapPanel.
Dock и Grid Окно традиционного Windows-приложения имеет довольно стандартную струк- туру. Меню приложения почти всегда располагается у верхнего края клиентской области и распространяется на всю ширину окна — другими словами, оно при- стыковано к верхнему краю клиентской области. Если программа содержит панель инструментов, последняя обычно тоже стыкуется между меню и верхним кра- ем клиентской области (естественно, непосредственно к краю прилегает только один пристыкованный объект). Строка состояния, если она есть, стыкуется к ниж- нему краю клиентской области. В Проводнике Windows дерево каталогов ото- бражается на отдельной панели, пристыкованной к левому краю клиентской области. Однако меню, панель инструментов и строка состояния обладают боль- шим приоритетом, чем панель с деревом каталогов, потому что они простирают- ся на всю ширину клиентской области, тогда как панель с деревом по вертикали занимает только то место, которое остается от других элементов. Для создания пристыкованных панелей в WPF включен класс DockPanel. Объект панели создается следующим образом: DockPanel dock = new DockPanelО; Если объект создается в конструкторе объекта Window, возможно, он тут же будет задан свойству Content окна: Content = dock: Построение макета окна часто начинается с объекта DockPanel, к которому (если потребуется) присоединяются дочерние панели других типов. Для добав- ления элемента управления (или другого интерфейсного элемента) Ctrl к панели DockPanel используется такой же синтаксис, как и для других видов панелей: dock.Children.Add(ctrl): Но после этого необходимо указать, к какой стороне DockPanel должен быть пристыкован объект Ctrl. Например, команда стыковки Ctrl у правого края кли- ентской области выглядит так: DockPanel.SetDock(ctrl, Dock.Right): Будьте внимательны: эта команда совершенно не относится к только что со- зданному объекту класса DockPanel с именем dock. Метод SetDock является стати-
Dock и Grid 115 ческим методом класса DockPanel. При вызове ему передаются два аргумента: стыкуемый элемент и одно из значений перечисления Dock: Dock.Left, Dock.Top, Dock.Right и Dock.Bottom. Неважно, будет ли метод DockPanel,SetDock вызван до или после включения элемента в коллекцию Children объекта DockPanel. Более того, вызвать DockPanel. SetDock для некоторого элемента можно даже в том случае, если вы еще не со- здали объект DockPanel и не собираетесь это делать! В странном вызове SetDock используется присоединенное свойство; эта тема будет более подробно рассмотрена в главе 8. А пока достаточно сказать, что упо- мянутый вызов статического метода SetDock эквивалентен следующей команде: Ctrl.SetValue(DockPanel.DockProperty. Dock.Right): Метод SetValue определяется классом Dependencyobject (от которого наследу- ют многие классы WPF), а свойство DockPanel.DockProperty является статическим полем, доступным только для чтения. Это присоединенное свойство, значение которого (Dock.Right) сохраняется элементом. При определении макета окна объект DockPanel может получить информацию о состоянии стыковки элемента вызовом Dock dck = DockPanel.GetDock(Ctrl): который фактически эквивалентен Dock dck = (Dock) Ctrl.GetValue(DockPanel.DockProperty): Следующая программа создает объект DockPanel с 17 дочерними кнопками. DockAroundTheBlock.cs и II DockAroundTheBlock.cs (с) 2006 by Charles Petzold // using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: namespace Petzol d.DockAroundTheBlock class DockAroundTheBlock : Window { [STAThread] public static void MainO { Application app = new ApplicationO: app.Run(new DockAroundTheBlock()); } public DockAroundTheBlock()
116 Глава 6. Dock и Grid { Title = "Dock Around the Block"; DockPanel dock = new DockPanel 0; Content = dock; for (int j = 0; 1 < 17; i++) { Button btn = new ButtonO: btn.Content = "Button No. " + (i + 1): dock.Children.Add(btn): btn.SetValue(DockPanel .DockProperty, (Dock)(i M)); } } } } В этой программе команда DockPanel.SetDock(btn, (Dock)(i И)); последовательно присваивает каждой кнопке значения из перечисления Dock; Dock.Left, Dock.Top, Dock.Right и Dock.Bottom (числовые коды 0, 1, 2 и 3). Как не- трудно убедиться, элементы, добавленные первыми, обладают большим приори- тетом по «захвату» стороны DockPanel. Размер каждой кнопки подбирается по содержимому в одном направлении и растягивается до предела в другом на- правлении. Элемент, добавленный последним (строка «Button No. 17»), вообще не сты- куется, а занимает все оставшееся внутреннее пространство. Такое поведение обусловлено используемым по умолчанию значением свойства LastChildFill клас- са DockPanel. Вызов DockPanel.SetDock для последней кнопки игнорируется. По- пробуйте добавить команду dock.LastChildFill = false; в любой момент после создания DockPanel — вы увидите, что последняя кнопка тоже стыкуется, а оставшееся свободное пространство остается незаполненным. Итак, панель DockPanel заполняется от краев к середине. Первые дочерние элементы размещаются у краев родителя, а последующие — ближе к центру. Как правило, дочерние элементы DockPanel растягиваются по всей ширине (или высоте) панели, потому что свойства HorizontalAlignment и VerticalAlignment до- черних элементов по умолчанию равны Stretch. Попробуйте вставить следую- щую команду; btn.HorizontalAlignment = HorizontalAlignment.Center; Происходит нечто странное — кнопки, пристыкованные у верхнего и нижнего края (а также последняя кнопка в центре), уменьшаются по ширине. Несомнен- но, такое поведение не соответствует общепринятым канонам пользовательско- го интерфейса (как, впрочем, и задание внешних отступов для пристыкованных элементов).
Dock и Grid 117 В следующей программе DockPanel используется более традиционным способом. Внутри панели размещаются элементы Menu, ToolBar, StatusBar, ListBox и TextBox, но поскольку эти элементы будут более подробно описаны в последующих гла- вах, сейчас они фактически остаются на уровне заготовок. Так как TextBox явля- ется последним дочерним элементом, он заполняет все пространство, не задей- ствованное другими элементами. MeetTheDockers.es // // MeetTheDockers.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; namespace Petzold.MeetTheDockers { public class MeetTheDockers : Window { [STAThread] public static void MainQ { Application app = new Application); app.Run(new MeetTheDockersO); } public MeetTheDockersO { Title = "Meet the Dockers"; DockPanel dock = new DockPanel(); Content = dock; // Создание меню Menu menu = new MenuО; Menuitem item = new MenuItemO; item.Header = "Menu"; menu.Iterns.Add(item); // Размещение меню у верхнего края панели DockPanel.SetDock(menu, Dock.Top); dock.Children.Add(menu); // Создание панели инструментов ToolBar tool = new ToolBarO;
118 Глава 6. Dock и Grid tool.Header = "Toolbar"; // Размещение панели инструментов у верхнего края DockPanel.SetDock(tool, Dock.Top); dock.Children.Add(tool); // Создание строки состояния StatusBar status = new StatusBarO: StatusBarltem statitem = new StatusBarltemO; statitem.Content = "Status"; status.Items.Add(statitem); // Размещение строки состояния у нижнего края панели DockPanel.SetDock(status, Dock.Bottom); dock.ChiIdren.Add(status); // Создание списка ListBox Istbox = new ListBoxO; Istbox.Items.Add("List Box Item"); // Размещение списка у левого края панели DockPanel .SetDock(Istbox, Dock.Left); dock.Chi 1dren.Add(1stbox); // Создание текстового поля TextBox txtbox = new TextBoxO; txtbox.AcceptsReturn = true; // Размещение текстового поля и передача фокуса dock.Chi 1dren.Add(txtbox); txtbox. FocusO; } } } Конечно, программа получается весьма несовершенной, потому что ни один из элементов не выполняет никаких полезных функций, но у нее есть и дру- гой, менее очевидный недостаток. В нормальном приложении элементы ListBox и TextBox должны быть разделены вешкой разбивки (splitter), чтобы пользова- тель мог отрегулировать относительные размеры элементов. Вешка разбивки реализуется панелью Grid, но это не единственная причина для использования Grid. Эта панель отображает свои дочерние элементы в виде набора ячеек, упорядоченных по строкам и столбцам. Казалось бы, подобную структуру можно реализовать методом, сходным с тем, что использовался в про- грамме StackThirtyButtons предыдущей главы. Однако отображаемые элементы довольно часто должны выравниваться как по столбцам, так и по строкам. Если бы кнопки StackThirtyButtons отличались размерами, это нарушило бы их вы- равнивание.
Dock и Grid 119 Пожалуй, Grid — самая полезная из всех макетных панелей, но она же и самая сложная. Хотя Grid может рассматриваться как единое, универсальное решение, разработчик все же должен действовать более гибко — WPF не зря поддержива- ет разные типы панелей. Использование панели Grid в программе начинается с создания объекта: Grid grid = new GridO: Вероятно, во время экспериментов с Grid стоит включить разделительную сетку: grid.ShowGridLines = true: При установке этого свойства ячейки разделяются пунктирными линиями. Изменить стиль, цвет или толщину линий сетки невозможно. Если вы хотите управлять видом линий на уровне, подходящем для печатного документа, вероят- но, вместо Grid стоит воспользоваться Table. Класс Table (определяемый в про- странстве имен System.Windows.Documents) больше похож на традиционные табли- цы, встречающиеся в HTML или текстовых редакторах. Класс Grid предназначен исключительно для размещения содержимого панели. Объекту Grid необходимо указать количество столбцов и строк, а также их размеры. Высота каждой строки определяется одним из нескольких способов: О фиксированная высота в аппаратно-независимых единицах; О выравнивание по самому высокому дочернему элементу строки; О вычисление по количеству оставшегося места (вероятно, с пропорциональ- ным распределением между другими строками). Аналогичным образом определяется ширина каждого столбца. Эти три вари- анта соответствуют трем элементам перечисления GridUnitType: Pixel, Auto и Star (название последнего происходит от таблиц HTML, где символ * обозначает все оставшееся место). Высота строк и ширина столбцов задаются при помощи класса GridLength. Класс имеет два конструктора. Если при вызове задается только численное зна- чение: new GridLength(100) предполагается, что число определяет размер в аппаратно-независимых едини- цах. Приведенный вызов конструктора эквивалентен new GridLength(100. GridUnitType.Pixel) Также число может быть задано с элементом перечисления GridUnitType.Star: new GridLength(50, GridUnitType.Star) В этом режиме выполняется пропорциональное распределение оставшегося места, но число используется только в сочетании с другими строками и столб- цами, использующими GridUnitType.Star. Также возможен вызов конструктора с указанием типа GridUnitType.Auto: new GridLength(0, GridUnitType.Auto)
120 Глава 6. Dock и Grid Однако в этом случае числовое значение игнорируется, и вместо вызова кон- структора можно воспользоваться статическим свойством GridLength.Auto, воз- вращающим объект типа GridLength. Класс Grid обладает свойствами RowDefinitions и ColumnDefinitions. Эти свойства относятся к типам RowDefinitionCollection и ColumnDefinitionCollection и представля- ют собой коллекции объектов RowDefinition и ColumnDefinition. Для каждой строки Grid создается объект RowDefinition. Центральное место в нем занимает свойство Height, значением которого является объект GridLength. (Класс RowDefinition также обладает свойствами MinHeight и MaxHeight, используе- мыми для ограничения высоты, но эти свойства не обязательны.) У аналогично- го объекта ColumnDefinition свойству Width задается объект GridLength, и он также включает свойства MinWidth и MaxWidth. К сожалению, код подготовки коллекций RowDefinitions и ColumnDefinitions по- лучается довольно объемистым. Следующий фрагмент определяет четыре строки: RowDefinition rowdef = new RowDefinitionО; rowdef.Height = GridLength.Auto; grid.RowDefi ni ti ons.Add(rowdef); rowdef = new RowDefinitionO; rowdef.Height = new GridLength(33, GridUnitType.Star); grid.RowDefi ni ti ons.Add(rowdef): rowdef = new RowDefinitionO: rowdef.Height = new GridLength(150): gri d.RowDefi ni ti ons.Add(rowdef); rowdef = new RowDefinitionO; rowdef.Height = new GridLength(67, GridUnitType.Star); grid.RowDefi ni ti ons.Add(rowdef); Высота первой строки определяется максимальной высотой ее элементов. Высота третьей строки составляет 150 аппаратно-независимых единиц. В зави- симости от высоты панели Grid может остаться свободное место; в таком случае 33 процента выделяется второй строке, а 67 процентов — четвертой. Пропорции не обязательно задавать в процентах, хотя код от этого становится более понят- ным. Элемент Grid просто суммирует все числовые значения GridUnitType.Star, а затем делит каждое число на сумму, чтобы определить долю каждой строки. Определения столбцов выглядят аналогично, только вместо Height в них зада- ется свойство Width. Возможно, вам удастся разместить некоторые из них в цикле. По умолчанию для свойств Height и Width используется режим GridUnitType.Star с числовым значением 1; чтобы пространство равномерно распределялось ме- жду строками и столбцами, можно воспользоваться значениями по умолчанию. Впрочем, для каждой строки или столбца все равно необходимо создать объект RowDefinition и ColumnDefinition. В этом случае код сводится к компактным коман- дам вида grid.RowDefinitions.Add(new RowDefinition()):
Dock и Grid 121 Если панель Grid состоит из единственного столбца, добавлять объект Column- Definition не обязательно. Аналогично, для панели Grid с одной строкой не нужно добавлять объект RowDefinition, а для панели Grid с одной ячейкой не понадобит- ся ни один из этих объектов. Добавление элементов на панель Grid производится точно так же, как и для любой другой панели: grid.ChiIdren.AddCctrl); Затем разработчик задает строку и столбец для отображения элемента при помощи статических методов SetRow и SetColumn: Grid.SetRow(ctrl. 2); Grid.SetColumn(ctrl, 5); В приведенном примере элемент помещается в третью строку и шестой стол- бец (нумерация начинается с 0). Как и в случае с DockPanel.SetDock, в этих вызо- вах используются присоединенные свойства. По умолчанию используется стро- ка и столбец с номером 0. В одной ячейке можно разместить несколько объектов, но в таких случаях часть объектов обычно скрывается. Если вы не видите элемент, заведомо разме- щенный на панели Grid, вероятно, он был размещен в одной строке и столбце с уже существующим элементом. Расположение элемента внутри ячейки опреде- ляется его свойствами HorizontalAlignment и VerticalAlignment. Следующая программа создает объект Grid, занимающий всю клиентскую об- ласть. Размеры окна подгоняются под размеры Grid, а рамка изменения размеров окна отключается. Программа создает сетку из трех строк и двух столбцов, при- чем во всех случаях используется режим GridLength.Auto. В ячейках размещаются четыре элемента Label и два элемента TextBox, в которых вводятся даты. Про- грамма вычисляет разность между двумя введенными датами. CalculateYourLife.cs // 1 И CalculateYourLife.cs (с) 2006 by Charles Petzold //--------------------------------------------------- using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input; using System.Windows.Media: namespace Petzold.CalculateYourLife { class CalculateYourLife : Window { TextBox txtboxBegin, txtboxEnd: Label IblLifeYears:
122 Глава 6. Dock и Grid [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new CalculateYourLifeO): public CalculateYourLifeO { Title = "Calculate Your Life"; SizeToContent = SizeToContent.WidthAndHeight; ResizeMode = ResizeMode.CanMinimize: // Создание объекта Grid Grid grid = new GridO; Content = grid; // Определения строк и столбцов for (int i = 0; i < 3; i++) { RowDefinition rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; grid.RowDefinitions.Add(rowdef); } for (int i = 0: i < 2; i++) { ColumnDefinition coldef = new ColumnDefinitionO: col def.Width = GridLength.Auto; gri d.ColumnDefini ti ons.Add(col def): } // Первый объект Label Label Ibl = new Label(); Ibl.Content = "Begin Date: grid.Children.Add(lbl); Grid.SetRow(Ibl. 0); Grid.SetColumndbl, 0); // Первый объект TextBox txtboxBegin = new TextBoxO; txtboxBegin.Text = new DateTime(1980, 1, 1).ToShortDateStringO; txtboxBegin.TextChanged += TextBoxOnTextChanged: grid.ChiIdren.Add(txtboxBegin); Grid.SetRow(txtboxBegin, 0); Grid.SetColumn(txtboxBegin, 1); // Второй объект Label Ibl = new Label(); Ibl.Content = “End Date:
Dock и Grid 123 grid.Children.Adddbl); Grid.SetRowdbl, 1): Grid.SetColumndbl. 0): // Второй объект TextBox txtboxEnd = new TextBoxO; txtboxEnd.TextChanged += TextBoxOnTextChanged: gri d.Chi 1dren.Add(txtboxEnd); Grid.SetRow(txtboxEnd. 1): Grid.SetColumn(txtboxEnd, 1): // Третий объект Label Ibl = new Label(); 1 bl.Content = "Life Years: ": grid.Children.Adddbl); Grid.SetRowdbl, 2): Grid.SetColumndbl, 0): // Объект Label для вычисленного результата IblLifeYears = new Label(); gri d.Chi 1dren.Add(1 bl Li feYears): Grid.SetRowdblLifeYears, 2): Grid.SetColumn(IblLifeYears, 1); // Задание внешних отступов Thickness thick = new Thickness(5); // -1/20 дюйма grid.Margin = thick: foreach (Control Ctrl in grid.Children) Ctrl.Margin = thick: // Передача фокуса и инициирование события txtboxBegin.Focusd: txtboxEnd.Text = DateTime.Now.ToShortDateString(): } void TextBoxOnTextChanged(object sender, TextChangedEventArgs args) { DateTime dtBeg, dtEnd: if (DateTime.TryParse(txtboxBegin.Text, out dtBeg) && DateTime.TryParse(txtboxEnd.Text, out dtEnd)) int iYears = dtEnd.Year - dtBeg.Year: int iMonths = dtEnd.Month - dtBeg.Month; int iDays = dtEnd.Day - dtBeg.Day; if (iDays < 0) iDays += DateTime.DaysInMonth(dtEnd.Year, 1 + (dtEnd.Month + 10) % 12): iMonths -= 1:
124 Глава 6. Dock и Grid } if (IMonths < 0) { IMonths += 12: iYears -= 1; } IblLifeYears.Content = String.Format("{0} year{l}, {2} month{3}. {4} day{5}", iYears, IYears == 1 ? "" : “s". IMonths. IMonths == 1 ? "" : "s". IDays. Ways == 1 ? "" : "s"): } else { 1 bl Li feYears. Content = } } } } Программа устанавливает обработчики события TextChanged для двух эле- ментов TextBox. Как подсказывает название, это событие срабатывает при каж- дом изменении содержимого текстового поля. После преобразования текстовых строк в объекты DateTime обработчик вычисляет разность между двумя датами. Обычно при вычитании объектов DateTime вы получаете объект TimeSpan, но в данном примере я хотел, чтобы программа вычисляла разность двух дат так, как это делает обычный человек (в годах, месяцах и днях). Задача решается ме- тодом DaysInMonth класса DateTime и делением с остатком. Рассмотрим другой пример использования класса Grid. Программа создает две панели Grid как дочерние объекты для StackPanel. Первая панель Grid состоит из двух столбцов и пяти строк для надписей и элементов TextBox, а вторая — из одной строки и двух столбцов для кнопок ОК и Cancel. EnterTheGrid.cs //---------------------------------------------- // EnterTheGrid.cs (с) 2006 by Charles Petzold //---------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.EnterTheGrid { public class EnterTheGrid ; Window {
Dock и Grid 125 [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new EnterTheGridO): public EnterTheGridO { Title = “Enter the Grid"; MinWidth = 300; SizeToContent = SizeToContent.WidthAndHeight; // Создание объекта StackPanel для содержимого окна StackPanel stack = new StackPanelО; Content = stack; // Создание объекта Grid и его добавление в StackPanel Grid gridl = new GridO; gridl.Margin = new Thickness(5); stack.Children.Add(gridl); // Создание определений строк for (int i = 0; i < 5; 1++) { RowDefinition rowdef = new RowDefinitionО; rowdef.Height = GridLength.Auto; gri dl.RowDefi ni ti ons.Add(rowdef): } // Создание определений столбцов ColumnDefinition coldef = new ColumnDefinitionO; coldef.Width = GridLength.Auto; gri dl.ColumnDefi ni ti ons.Add(col def); coldef = new ColumnDefinitionO; coldef.Width = new GridLengthdOO, GridUnitType.Star); gridl.ColumnDefi ni ti ons.Add(col def); // Создание надписей и текстовых полей string[] strLabels = { "_First name:", "_Last name:", "_Social security number:", "_Credit card number:", "_0ther personal stuff:" }: for(int i = 0; i < strLabels.Length; i++) { Label Ibl = new Label(); Ibl.Content = strLabelsEi]: Ibl.VerticalContentAlignment = VerticalAlignment.Center; gridl.Children.Adddbl):
126 Глава 6. Dock и Grid Grid.SetRowdbl, 1): Grid.SetColumn(Ibl, 0); TextBox txt box = new TextBoxd; txtbox.Margin = new Thickness(5); gridl.Chi 1dren.Add(txtbox); Grid.SetRow(txtbox. i); Grid.SetColumn(txtbox, 1); } // Создание второго объекта Grid и добавление в StackPanel Grid grid2 = new Gridd; grid2.Margin = new Thickness(lO): stack.Chi 1dren.Add(gri d2); // Для одной строки создавать определение не обязательно. // В определениях строк по умолчанию используется режим "star" grid2.ColumnDefinitions.Add(new ColumnDefinltlonO): grid2.Col umnDefi nitions.Add(new ColumnDefi ni ti on()): // Создание кнопок Button btn = new ButtonO; btn.Content = "Submit"; btn.Horizontal Alignment = HorizontalAlignment.Center; btn.IsDefault = true; btn.Click += delegate { Closed; }; grid2.Children.Add(btn); // Строка и столбец 0 btn = new ButtonO; btn.Content = "Cancel"; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.IsCancel = true; btn.Click += delegate { Closed; }; grid2.Chi 1dren.Add(btn); Grid.SetColumn(btn. 1); // Строка 0 // Передача фокуса первому текстовому полю (stack.Children[0] as Panel).Children[l].Focus(); } } } Работа программы зависит от стандартного поведения интерфейсных элемен- тов по заполнению родительской области. Панель StackPanel занимает полную ширину окна, как и две панели Grid. В верхней панели Grid первый столбец (с над- писями) обладает шириной GridLength.Auto, а второй — шириной GridUnitType.Star; это означает, что они занимают все оставшееся свободное место. В результате при увеличении ширины окна элементы TextBox автоматически расширяются. Элементы TextBox (и окно) тоже расширяются при вводе текста, выходящего за
Dock и Grid 127 рамки начальной ширины. Окну назначается минимальная ширина в 300 еди- ниц, чтобы элементы TextBox не выглядели слишком мелкими при запуске про- граммы. Высота первых пяти строк определяется высотой элементов TextBox. Чтобы элементы Label выравнивались по центру текстовых полей, программа задает свойству VerticalContentAlignment каждого объекта Label значение Center. Для размещения кнопок ОК и Cancel вместо однострочной панели Grid также можно было воспользоваться горизонтальной панелью StackPanel. Однако следу- ет заметить, что ширинам обоих столбцов назначен режим GridUnitType.Star, по- этому с увеличением ширины окна относительное расстояние между кнопками сохраняется. На панели Grid элемент может занимать несколько смежных строк и/или столб- цов. Количество столбцов и строк, занимаемых элементом, задается статически- ми методами Grid.SetRowSpan и Grid.SetColumnSpan. Пример кода: grid.Children.Add(ctrl); Grid.SetRowCctrl, 3); Grid.SetRowSpan(ctrl, 2); Grid.SetColumn(ctrl. 5): Grid.SetColumnSpan(ctrl. 3): Элемент Ctrl занимает область из шести ячеек, находящуюся на пересечении строк 3 и 4 и столбцов 5, 6 и 7 (напомню, что нумерация начинается с 0). Следующая программа использует примерно такое же расположение элемен- тов, но использует всего одну панель Grid из шести строк и четырех столбцов. Первые пять строк содержат надписи и текстовые поля, а в последней строке находятся кнопки. SpanTheCells.cs //--------------------------------------------- // SpanTheCells.cs (с) 2006 by Charles Petzold //--------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.SpanTheCells { public class SpanTheCells : Window [STAThread] public static void MainO { Application app = new Application); app. Run (new SpanTheCellsO):
128 Глава 6. Dock и Grid } public SpanTheCellsO { Title = "Span the Cells": SizeToContent = SizeToContent.WidthAndHeight: // Создание объекта Grid Grid grid = new GridO: grid.Margin = new Thickness(5): Content = grid: // Создание определений строк for (int i = 0: i < 6: i++) RowDefinition rowdef = new RowDefinition(): rowdef.Height = GridLength.Auto: grid.RowDefi ni ti ons.Add(rowdef): } // Создание определений столбцов for (int i = 0: i < 4: i++) ColumnDefinition coldef = new ColumnDefinitionO: if (i == 1) coldef.Width = new GridLengthdOO, GridUnitType.Star); else col def.Width = GridLength.Auto: gri d.ColumnDefi ni ti ons.Add(col def): } // Создание надписей и текстовых полей string[] astrLabel = { "_First name:", "_Last name:", "_Social security number:", "-Credit card number:", "_Other personal stuff:" }; for(int i = 0: i < astrLabel.Length: i++) { Label Ibl = new Label0; Ibl.Content = astrLabel[1]: Ibl.VerticalContentAlignment = Vertical Alignment.Center: grid.Children.Adddbl); Grid.SetRowdbl, i); Grid.SetColumnObl, 0): TextBox txtbox = new TextBoxO: txtbox.Margin = new Thickness(5):
Dock и Grid 129 grid.Children.Add(txtbox): Grid.SetRow(txtbox, i): Grid.SetColumn(txtbox, 1); Grid.SetColumnSpan(txtbox, 3); } // Создание кнопок Button btn = new ButtonO: btn.Content = "Submit"; btn.Margin = new Thickness(5): btn.IsDefault = true; btn.Click += delegate { Closet); }; grid.Children.Add(btn); Grid.SetRowtbtn. 5): Grid.SetColumn(btn, 2): btn = new ButtonO; btn.Content = "Cancel"; btn.Margin = new Thickness(5): btn.IsCancel = true: btn.Click += delegate { CloseO: }: grid.Children.Add(btn); Grid.SetRowtbtn, 5), Grid.SetColumn(btn. 3); // Передача фокуса первому текстовому полю grid.Children[l].Focus О: } } } Обратите внимание: программа задает всем шести строкам, а также всем че- тырем столбцам, кроме второго, режим выбора размера GridLength.Auto. Второму столбцу назначается режим GridLengthType.Star, чтобы он использовал все остав- шееся место. Панель Grid пригодится и в тех случаях, когда отображаемые элементы долж- ны разделяться вешкой разбивки — toijkoi’i полосой, при помощи которой поль- зователь регулирует распределение пространства между двумя областями окна. Объект GridSplitter должен быть дочерним объектом панели Grid, а его пози- ция (строка, столбец и возможное занятие нескольких ячеек) задаются так же, как для любого другого дочернего объекта Grid: GridSplitter split = new GridSplitter(); split.Width = 6: grid.Children.Add(spl it); Grid.SetRow(split, 3): Grid.SetColumn(split. 2);
130 Глава 6. Dock и Grid Впрочем, существует одна странность: GridSplitter может занимать ячейку со- вместно с другим элементом и частично скрыть элемент в этой ячейке (или элемент в ячейке может полностью скрыть GridSplitter). Чтобы избежать подоб- ных проблем, снабдите элемент в ячейке небольшими внешними отступами или включите GridSplitter в коллекцию дочерних объектов после элемента в ячейке, чтобы вешка разбивки находилась на переднем плане. По умолчанию у объекта GridSplitter свойство HorizontalAlignment равно Right, а свойство VerticalAlignment равно Stretch, в результате чего вешка разбивки ото- бражается в виде вертикальной полосы у правого края ячейки. По умолчанию ширина GridSplitter равна 0, поэтому ей нужно задать небольшую положительную величину (например, 6 единиц). После этого вы сможете перемещать вешку для изменения ширины столбца, в котором она находится, и ширины столбца спра- ва от вешки. Следующая программа создает таблицу из кнопок размером 3x3 и помещает в центральную ячейку вешку разбивки. SplitNine.cs //------------------------------------------- // SplitNine.cs (с) 2006 by Charles Petzold //------------------------------------------- using System; using System.Windows: using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.SplitNine { public class SplitNine : Window { [STAThread] public static void MainO { Application app = new Application); app.Run(new SplitNineO); } public SplitNineO { Title = "Split Nine"; Grid grid = new Grid(); Content = grid; // Создание определений строк и столбцов for (int 1 = 0; 1 < 3: 1++) { grid.ColumnDefini ti ons.Add(new Col umnDef 1 ni ti on()); grid.RowDefi ni ti ons.Add(new RowDefi n11 i on()); }
Dock и Grid 131 // Создание 9 кнопок for (int х = 0; х < 3; x++) for (int у = 0; у < 3: y++) { Button btn = new ButtonO: btn.Content = "Row " + у + " and Column " + x; grid.Children.Add(btn): Grid.SetRow(btn, y): Grid.SetColumn(btn, x): } // Создание вешки разбивки GridSplitter split = new GridSplitterO: split.Width = 6; grid.Children.Add(split): Grid.SetRow(split, 1): Grid.SetColumn(split, 1); } } } Запустив программу, вы увидите, что вешка разбивки отображается у право- го края центральной ячейки и скрывает правый край кнопки. Чтобы отделить вешку от кнопки, можно задать для кнопок небольшие внешние отступы: btn.Margin = new Thickness(10); Но и такое решение выглядит немного странно. Конечно, даже если вешка разбивки находится всего в одной ячейке, ее пере- мещение приводит к изменению ширины столбца во всех строках. Было бы ра- зумнее растянуть вешку на полную высоту таблицы: Grid.SetRow(split, 0): Grid.SetColumn(split. 1): Grid.SetRowSpan(split, 3): Но для экспериментальных целей давайте оставим вешку в одной ячейке. Важнейшими свойствами класса GridSplitter являются свойства HorizontalAlignment и VerticalAlignment. Они управляют ориентацией вешки (горизонтальная или вер- тикальная), ее местонахождением в ячейке и строками/столбцами, находящи- мися под ее управлением. С параметрами по умолчанию (HorizontalAlignment= Right, VerticalAlignment=Left) вешка располагается у правого края ячейки. Переме- щение вешки приводит к перераспределению доступной ширины между столб- цом, в котором находится вешка, и столбцом справа от него. Следующая команда изменяет тип выравнивания вешки разбивки: split.HorizontalAlignment = Horizontal Alignment.Left: Теперь вешка распределяет пространство между столбцом, в котором она на- ходится, и столбцом слева от него. Также опробуйте следующую команду: split.HorizontalAlignment = HorizontalAlignment.Center:
132 Глава 6. Dock и Grid Теперь вешка располагается по центру средней кнопки, а ее перемещение из- меняет не ширину центрального столбца, а ширины двух столбцов по обе сто- роны от вешки. На первый взгляд такое использование вешки разбивки кажется совершенно невероятным, но в действительности это ключ к разумному исполь- зованию GridSplitter. Если бы ячейки центрального столбца не содержали кно- пок, а ячейке был назначен режим GridLength.Auto, то вешка разбивки смотрелась и работала бы вполне нормально. Вы уже видели, как вешка разбивки влияет на изменение ширин пар столб- цов в зависимости от значения HorizontalAlignment: Right (по умолчанию), Left или Center. Поведение вешки можно переопределить при помощи свойства Resize- Behavior класса GridSplitter. Свойству задаются значения из перечисления Grid- ResizeBehavior. Элементы перечисления указывают, на какие столбцы распро- страняется действие вешки по отношению к текущему столбцу (так называется столбец, в котором находится вешка). Чтобы сделать вешку горизонтальной, задайте свойству HorizontalAlignment значение Stretch, а свойству VerticalAlignment — значение Top, Center или Bottom. Например, следующий фрагмент обеспечивает отображение вешки разбивки у верхнего края ячейки: split.HorizontalAlignment = HorizontalAlignment.Stretch: split.VerticalAlignment = VerticalAlignment.Top: split.Height = 6: Перемещение вешки перераспределяет место между строкой, в которой нахо- дится вешка, и строкой над ней. Как было показано ранее, вешка располагается вертикально, если свойст- во VerticalAlignment задано равным Stretch, или горизонтально, если свойство HorizontalAlignment задано равным Stretch. Вертикальная вешка изменяет ширину столбцов, а горизонтальная — высоту строк. Связь между внешним видом веш- ки и ее функциональностью задается свойством ResizeDirection, принимающим значения из перечисления GridResizeDirection: Columns, Rows или Auto (по умолча- нию). Попробуйте выполнить следующий фрагмент: split.HorizontalAlignment = HorizontalAlignment.Stretch: split.VerticalAlignment = VerticalAlignment.Top: split.ResizeDirection = GridResizeDirection.Columns: Теперь вешка располагается горизонтально, но перемещать ее вверх/вниз нельзя — она двигается только вперед/назад, но при этом изменяет ширину столбцов. Но и это еще не все! Если задать обоим свойствам HorizontalAlignment и Ver- ticalAlignment значение Stretch, вешка заполнит всю ячейку (не задавайте свойст- ва Width и Height). А можно вообще отказаться от задания Stretch свойствам HorizontalAlignment и VerticalAlignment и оформить вешку в виде манипулятора, раз- меры которого определяются свойствами Width и Height. Мои рекомендации просты: не размещайте GridSplitter в ячейке, занятой дру- гим элементом, и выделите всю панель Grid для реализации разбивки. Если вам
Dock и Grid 133 нужна вертикальная вешка, создайте объект Grid с тремя столбцами. Задайте среднему столбцу режим GridLength.Auto и поместите вешку в этот столбец. Для горизонтальной вешки создается ооъект Grid с тремя строками. Средней строке задается режим GridLength.Auto, и вешка помещается в эту строку. В обоих слу- чаях содержимое двух других столбцов пли строк может быть произвольным (в частности, они могут содержать другие панели Grid). Следующая программа создает панель Grid из трех столбцов, содержащих кнопку, вертикальную вешку разбивки и панель Grid из трех строк. Последняя содержит еще одну кнопку, горизонтальную вешку разбивки и третью кнопку. SplitTheClient.cs //------------------------------------------------ // Spl1tTheClient.cs (с) 2006 by Charles Petzold //------------------------------------------------ using System; using System.Windows; us i ng System.Wi ndows.Cont го 1 s; using System.Windows.Input: using System.Windows.Media: class SplitTheC1 lent ; Window { [STAThread] public static void Main!) { Application app = new ApplicationO; app.Run(new Spl11TheClient()); public Spl1tTheC1 lento { Title = "Spl11 the Cilent"; // Панель Grid с вертикальной вешкой разбивки Grid gridl = new GridO. gridl.ColumnDef1 nitions.Add(new ColumnDef 1 nitionO); gridl.ColumnDeHnitions.Acidfnew ColumnDefinitionO); gr 1 dl. Co I umnDef i ni t i ons Add (new Col umnDef 11 ii 11 on Од- gridl .ColumnDefimtionsEl].Width - GridLength.Auto; Content = gridl; // Кнопка слева от вертикальной вешки Button btn = new ButtonO: btn.Content = "Button No. 1" • gr1d1.Ch11d ren.Add(btn), Grid.SetRow(btn. 0); Grid.SetColumn(btn, Од // Вертикальная вешка разбивки GridSplitter split - new GridSplitterO;
134 Глава 6. Dock и Grid spl1t.ShowsPreview = true; split.HorizontalAlignment = HorizontalAlignment.Center; split.VerticalAlignment = VerticalAlignment.Stretch; split.Width = 6; gridl.Children.Add(splIt); Gr1d.SetRow(spl1t, 0); Gr1d.SetColumn(spl1t. 1); // Панель Grid с горизонтальной вешкой Grid gr1d2 = new GrldO; grid2.RowDefinitions.Add(new RowDefi ni ti on()); grid2.RowDefinitions.Add(new RowDefinition()); grid2.RowDefinitions.Add(new RowDefini11 on()); gr1d2.RowDef1n1t1ons[l].Height = GridLength.Auto; gridl.Chi 1dren.AddCgrld2); Gr1d.SetRow(gr1d2, 0); Gr1d.SetColumn(gr1d2, 2); // Кнопка над горизонтальной вешкой btn = new ButtonO; btn.Content = "Button No. 2"; gr1d2.Children.Add(btn); Gr1d.SetRow(btn, 0); Gr1d.SetColumn(btn, 0); // Горизонтальная вешка split = new GrldSplItterO; splIt.ShowsPreview = true; split.HorizontalAlignment = HorizontalAlignment.Stretch; split.VerticalAlignment = VerticalAlignment.Center; split.Height = 6; gr1d2.Ch11dren.Add(sp!1t); Gr1d.SetRow(spl1t, 1); Gr1d.SetColumn(spl1t, 0); // Кнопка под горизонтальной вешкой btn = new ButtonO; btn.Content = "Button No. 3"; grid2.Children.Add(btn); Grid.SetRow(btn, 2); Gr1d.SetColumn(btn, 0); } } Программа задает свойству ShowsPreview объекта GridSplitter значение true. При перемещении вешки ячейки не изменяют размеры до тех пор, пока пользователь не отпустит кнопку мыши. Обратите внимание: клавишей Tab можно передать вешке фокус ввода, а затем перемещать ее клавишами со стрелками. Если шаг смещения вас не устроит, измените свойство Keyboardincrement.
Dock и Grid 135 Как вы, возможно, заметили, при изменении размера окон SplitNine и Split- TheClient происходит пропорциональное изменение высот строк и ширин столб- цов. Возможно, вы предпочтете, чтобы одна строка или столбец сохраняли фик- сированный размер, пока остальные изменяются? (Такое поведение встречается в Проводнике Windows: при изменении размеров окна левая панель сохраняет фиксированную ширину.) Равное перераспределение происходит тогда, ко- гда свойствам Height или Width строк и столбцов назначается режим GridLength- Type.Star. Чтобы строка или столбец сохраняли фиксированный размер при из- менении размера окна, используйте режим GridLengthType.Pixel или (несомненно, более распространенное решение) GridLength.Auto. Следующая программа демонстрирует эту методику, а также ряд других. Ее первая версия была написана для Windows 1.0 и опубликована в выпуске «Microsoft Systems Journal» за май 1987 года. Класс WPF ScrollBar наследует от Control через RangeBase (абстрактный класс, который также является предком ProgressBar и Slider). Полосы прокрутки мо- гут размещаться вертикально или горизонтально в зависимости от свойства Orientation. Свойство Value лежит в интервале допустимых значений от свойства Minumum до Maximum. Щелчок на стрелке в конце полосы прокрутки изменяет Value на величину, определяемую свойством SmallChange. Щелчок в области, прилегающей к бе- гунку, изменяет Value на величину LargeChange. Все эти числа относятся к типу double. Еще раз: все свойства ScrollBar, включая само текущее значение Value, явля- ются вещественными числами двойной точности, поэтому не удивляйтесь при виде дробных значений. Если вы хотите работать с целыми числами, преобра- зуйте данные, полученные от ScrollBar. Класс ScrollBar наследует событие ValueChanged от RangeBase, а также опреде- ляет событие Scroll. С событием передается дополнительная информация в виде элемента перечисления ScrollEventType. Например, если программа не успевает обрабатывать все перемещения бегунка, она может игнорировать все события типа ScrollEventType.ThumbTrack и получить окончательную позицию бегунка в со- бытии типа ScrollEventType.EndScrolL Программа ScrollCustomColors создает две панели Grid. Первая (gridMain) предназначена исключительно для реализации вертикальной вешки разбивки. Первая ячейка содержит вторую панель Grid (просто grid), на которой разме- щены шесть надписей и три полосы прокрутки. Центральная ячейка GridMain содержит объект GridSplitter, а последняя — панель StackPanel, предназначенную исключительно для отображения цвета фона. Исходный размер окна устанавливается равным 500 аппаратно-независимым единицам. При определении трех столбцов gridMain ячейке с полосами прокрут- ки и надписями назначается ширина 200 единиц, а ширина ячейки с панелью StackPanel определяется в режиме GridUnitType.Star. При увеличении или уменьшении окна изменяется только ширина панели StackPanel.
136 Глава 6. Dock и Grid ScrollCustomColors.es // // ScrollCustomColors.es (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; class ColorScroll : Window { ScrollBar[] scrolls = new ScrollBar[3]; TextBlock[] txtValue = new TextBlock[3]; Panel pnlColor; [STAThread] public static void MainO Application app = new ApplicationO; app.Run(new ColorScroll()); public ColorScroll() { Title = "Color Scroll"; Width = 500; Height = 300; // Панель gridMain содержит вертикальную вешку разбивки Grid gridMain = new GridO; Content = gridMain; // Определения столбцов gridMain ColumnDefinition coldef = new ColumnDefinitionO: coldef.Width = new GridLength(200, GridUnitType.Pixel); gridMai n.ColumnDefi nitions.Add(col def); coldef = new ColumnDefinitionO; coldef.Width = GridLength.Auto; gri dMa i n.ColumnDefi ni t i ons.Add(col def); coldef = new ColumnDefinitionO; coldef.Width = new GridLength(100, GridUnitType.Star); gri dMai n.ColumnDefi ni ti ons.Add(col def); // Вертикальная вешка разбивки GridSplitter split = new GridSplitterO;
Dock и Grid 137 split.HorizontalAlignment = HorizontalAlignment.Center; split.VerticalAlignment = VerticalAlignment.Stretch; split.Width = 6; gridMain.Children.Add(split); Grid.SetRow(split, 0); Grid.SetColumn(split, 1); // На панели справа от вешки отображается цвет фона pnlColor = new StackPanelО; pnlColor.Background = new SolidColorBrush(SystemColors.WindowColor); gridMain.Children.Add(pnlColor); Grid.SetRow(pnlColor, 0); Grid.SetColumn(pnlColor. 2); // Вторичная панель Grid слева от вешки Grid grid = new Grid(); gridMain.Children.Add(grid); Grid.SetRow(grid, 0); Grid.SetColumn(grid, 0); // Три строки: надпись, полоса прокрутки и надпись RowDefinition rowdef = new RowDefinitionO; rowdef.Height = GridLength.Auto; grid.RowDefi ni t i ons.Add(rowdef); rowdef = new RowDefinitionO; rowdef .Height = new GridLengthdOO. GridUnitType.Star); grid.RowDefi ni ti ons.Add(rowdef); rowdef = new RowDefinitionO; rowdef.Height = GridLength.Auto; grid.RowDefi ni ti ons.Add(rowdef); // Три столбца для красной, зеленой и синей составляющих for (int i = 0; i < 3; i++) { col def = new ColumnDefinition!): coldef.Width = new GridLength(33, GridUnitType.Star): grid.ColumnDefinitions.Add(coldef): } for (int i = 0: i < 3: i++) { Label Ibl = new Label(); Ibl.Content = new string[] { "Red", "Green", "Blue" }[i]; Ibl.HorizontalAlignment = HorizontalAlignment.Center; grid.Children.Adddbl);
138 Глава 6. Dock и Grid Grid.SetRow( 1 bl, 0); Grid.SetColumndbl, 1): scrolls[1] = new ScrolIBarO; scrolls[1].Focusable = true; scrolls[1].Orientation = Orientation.Vertical; scrollsEi].Minimum = 0; scrollsEi].Maximum = 255; scrol1s[1].Smal1 Change = 1: scrol1s[i].LargeChange = 16; scrollsEi].VaiueChanged += ScrollOnValueChanged: grid.Chi 1dren.Add(scrollsEi]); Grid.SetRow(scrollsEi], 1); Grid.SetColumn(scrolls[i], i); txtValueEi] = new TextBlockO; txtValueEi].TextAlignment = TextAlignment.Center; txtValueEi].HorizontalAlignment = HorizontalAlignment.Center; txtValueEi].Margin = new Thickness(5); gri d.Chi 1dren.Add(txtValue[i]); Grid.SetRow(txtValue[i], 2); Grid.SetColumn(txtValue[i], i); } // Инициализация полос прокрутки Color clr = (pnlCol or.Background as SolidColorBrush).Color; scrolIsEO].Value = clr.R; scrollsEi].Value = clr.G; scrolls[2].Value = clr.B; // Передача фокуса scrol1s CO].Focus(); } void Scrol10nValueChanged(object sender, RoutedEventArgs args) ScrollBar scroll = sender as ScrollBar; Panel pnl = scroll.Parent as Panel; TextBlock txt = pnl.ChildrenEl + pnl.Children.IndexOf(scroll)] as TextBlock; txt.Text = String.Format("{0}\n0x{0:X2}", (int)scroll.Value); pnlColor.Background = new SolidColorBrush( Color.FromRgb((byte) scrolIsEO].Value, (byte) scrollsEi].Value,(byte) scrollsE2].Value));
Роек и Grid 139 Обработчик события ValueChanged обновляет объект TextBlock, связанный с объ- ектом ScrollBar, значение которого изменилось, а затем вычисляет новый цвет по текущим значениям трех полос прокрутки. Кроме панели Grid, также существует похожая панель UniformGrid — таблица, столбцы которой всегда имеют одинаковую ширину, а строки — одинаковую вы- соту. С UniformGrid не связываются определения строк или столбцов RowDefinition и ColumnDefinition вместо них достаточно задать количество строк и столбцов в свойствах Rows и Columns. Дочерние объекты, добавляемые в UniformGrid, по- следовательно заполняют ячейки первой строки, затем второй и т. д. Свойство Rows и/или Columns может быть равно нулю; в этом случае UniformGrid подбирает подходящее значение, исходя из количества дочерних объектов. Пример использования панели UniformGrid приведен в следующей главе, где на ее основе будет реализована знаменитая головоломка «15».
Canvas Из всех вариантов макетирования панель Canvas в наибольшей степени напоми- нает традиционные графические среды. Разработчик сам указывает, где дол- жен находиться тот или иной элемент, задавая его координаты. Как и в других компонентах WPF, координаты задаются в аппаратно-независимых единицах 1 /96 дюйма по отношению к левому верхнему углу. Возможно, вы заметили, что сами элементы не обладают свойствами X, Y, Left или Тор. При использовании панели Canvas местонахождение дочерних элемен- тов задается статическими методами Canvas.SetLeft и Canvas.SetTop. По аналогии с методом SetDock класса DockPanel (а также методами SetRow, SetColumn, SetRow- Span и SetColumnSpan класса Grid), методы SetLeft и SetTop связываются с присо- единенными свойствами, определяемыми классом Canvas. При желании вместо них можно использовать методы Canvas.SetRight и Canvas.SetBottom для определе- ния правого или нижнего края дочернего элемента по отношению к правому или нижнему краю Canvas. Некоторые классы Shapes — а конкретно, Line, Path, Polygon и Polyline — уже со- держат координатные данные. Если добавить эти элементы в коллекцию Children панели Canvas без задания каких-либо координат, позиционирование элементов будет осуществляться на основании координатных данных элемента. Все коор- динаты, явно заданные вызовами SetLeft и SetTop, прибавляются к координатным данным элемента. Многие интерфейсные элементы, в том числе элементы управления, пра- вильно определяют свои размеры на панели Canvas. Впрочем, есть и исключения (например, классы Rectangle и Ellipse), для которых свойства Width и Height долж- ны задаваться явно. Свойства Width и Height также часто задаются для самой па- нели Canvas. Расположенные на панели Canvas элементы могут перекрываться — иногда это даже может быть желательно. Как было показано ранее, в ячейках панели Grid можно разместить сразу несколько элементов, но это затрудняет управле- ние их относительным расположением. На панели Canvas расположение элемен- тов находится под полным контролем разработчика. Элементы, первыми добав- ленные в коллекцию Children, перекрываются элементами, добавленными позже.
Canvas 141 Допустим, на кнопке должен отображаться синий квадрат размером 1,5 дюй- ма с закругленными краями, в центре которого находится желтая звезда разме- ром 1 дюйм. Вот как это делается. PaintTheButton.es и // Pa1ntTheButton.cs (с) 2006 by Charles Petzold //----------------------------------------------- using System; using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: using System.Windows.Shapes; namespace Petzold.PaintTheButton { public class PaintTheButton : Window { [STAThread] public static void Main!) { Application app = new Application!): app.Run(new PaintTheButtonO)- } public PaintTheButtonO Title = "Paint the Button": // Создание объекта Button как содержимого окна Button btn = new Button!): btn.Horizontal Alignment = Horizontal Alignment.Center: btn.VerticalAlignment = VerticalAlignment.Center: Content = btn: // Создание объекта Canvas как содержимого кнопки Canvas canv = new Canvas!); canv.Width = 144; canv.Height = 144: btn.Content = canv; // Создание Rectangle как дочернего объекта Canvas Rectangle rect = new Rectangle!): rect.Width = canv.Width: rect.Height = canv.Height: rect.RadiusX = 24: rect.RadiusY = 24:
142 Глава?. Canvas rect.Fill = Brushes.Blue; canv.Children.Add(rect); Canvas.SetLeft(rect, 0); Canvas.SetRight(rect, 0); // Создание Polygon как дочернего объекта Canvas Polygon poly = new PolygonO; poly.Fill = Brushes.Yellow: poly.Points = new PointCollectionO: for (int i = 0; i < 5: i++) { double angle = 1 * 4 * Math.PI / 5; Point pt = new Point(48 * Math.Sin(angle). -48 * Math.Cos(angle)): poly.Points.Add(pt): } canv.Children.Add(poly): Canvas.SetLeft(poly, canv.Width / 2); Canvas.SetTop(poly, canv.Height / 2); } } } Программа создает объект Button и назначает его содержимым окна. Затем создается квадратный объект Canvas, который назначается содержимым кнопки. Поскольку свойствам HorizontalAlignment и VerticalAlignment кнопки задано значе- ние Center, кнопка автоматически изменяет свои размеры по размерам панели Canvas. Далее программа создает объект Rectangle и задает его свойствам Width и Height те же значения, что у Canvas. Объект включается в коллекцию дочерних объек- тов объекта Canvas так же, как другие панели: canv.Chi 1dren.Add(rect); Следующий код обеспечивает размещение объекта Rectangle в левом верхнем углу панели Canvas: Canvas.SetLeft(rect, 0): Canvas.SetRight(rect, 0): Строго говоря, эти две команды не обязательны, потому что значение 0 ис- пользуется по умолчанию. На следующем шаге создается объект многоугольника Polygon. Класс Polygon определяет свойство Points типа Pointcollection для хранения вершин многоуголь- ника. В только что созданном объекте Polygon свойство Points равно null; необ- ходимо явно создать объект типа Pointcollection и задать его свойству Points. Программа PaintTheButton демонстрирует один из способов, с использованием непараметризованного конструктора Pointcollection. Точки добавляются в коллек- цию методом Add.
143 Canvas Класс Pointcollection также определяет конструктор с аргументом типа lEnumer- able<Point>. В нем может передаваться как массив объектов Point, так и коллек- ция List<Point>. Цикл for вычисляет вершины звезды. Координаты точек лежат в интервале от -48 до 48 (центр эллипса находится в точке (0, 0)). Чтобы эллипс отображал- ся по центру Canvas, следующий код смещает все вершины многоугольника на половину ширины и высоты Canvas* Canvas.SetLeft(poly, canv.Width / 2); Canvas.SetTop(poly, canv.Height / 2); Конечно, класс Canvas может использоваться для размещения элементов в окне, но в таких ситуациях он обычно скорее мешает, чем помогает. Canvas хорошо подходит для отображения графики (см. часть 2) и рисования, управляемого мы- шью (см. главу 9), но для решения общих задач макетирования лучше использо- вать другие средства. Следующий класс, производный от Canvas, предназначен для реализации плит- ки в известной головоломке «15». Константа с именем SIZE определяет размер плитки (2/3 дюйма), а другая константа BORD — толщину границы (1/16 дюй- ма). Прорисовка границы создает иллюзию объема. Традиционно объекты на экране монитора прорисовываются так, как если бы источник света находился в левом верхнем углу. Граница состоит из двух объектов Polygon, окрашиваемых кистью SystemColors.ControlLightLightBrush (верхняя и левая стороны) и SystemColors. ControlDarkBrush (правая и нижняя стороны). Внутренняя часть плитки выглядит рельефной. Tile.cs //-------------------------------------- // T11e.cs (с) 2006 by Charles Petzold //-------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.PlayJeuDeTacquIn { public class Tile : Canvas const Int SIZE =64; // 2/3 дюйма const Int BORD = 6; // 1/16 дюйма TextBlock txtblk; public TlleO Width = SIZE;
144 Глава 7. ( Height = SIZE; // Граница на левой и верхней сторонах Polygon poly = new PolygonO; poly.Points = new Pointedlectlonfnew Po1nt[] { new Pol nt(0. 0). new Pol nt(SIZE, 0). new Pol nt(SIZE-BORD. BORD). new Pol nt(BORD, BORD), new Pol nt (BORD. SIZE-BORD). new' Po1nt(0, SIZE) }): poly.Fil1 = SystemColors.ControlLIghtLightBrush; Children.Add(poly); // Граница на правой и нижней сторонах poly =- new PolygonO; poly7. Points = new PolntCol lection (new Polntf] { new Po1nt(SIZE. SIZE), new PoinUSIZE. 0). new Point(SIZE-BORD. BORD). new Pol nt(SI ZE-BORD. SIZE BORD). new Point(BORD. SIZE-BORD), new Po1nt(0, SIZE) }): poly.Fill = SystemColors.ControlDarkBrush; Children.Add(poly); // Поле для вывода отцентрованного текста Border bord = new BorderO; bond.Width = SIZE - 2 * BORD: bord.Height = SIZE - 2 * BORD; bord.Background - SystemColors Control Brush: Children.Add(bord); SetLeEt(bord. BORD): SetTop(bord. BORD): // Вывод текста txtblk = new' TextBlocki) ; txtblk.FontSize = 32: txtblk.Foreground = SystemColors.ControlTextBrush: txtblk.Horizontal AlIgnment = HorlzontaI AlIgnment.Center: txtblk.VerticalAlignment = VerticalAlIgnment.Center; bord.Chi Id = txtblk; } // Открытое свойство для назначения текста public string text {
145 Canvas set { txtblk.Text = value: } get { return txtblk.Text; } } } } Обратите внимание, как в программе создается коллекция Points объектов Polygon: весь массив Point определяется в конструкторе Pointcollection! Класс Tile также должен вывести текст в центре каждой плитки. Вместо того чтобы пытаться вычислить фактический размер элемента TextBlock и отцен- тровать его на панели Canvas, класс использует более простое решение. После создания границы из двух многоугольников Tile создает объект типа Border — класса, наследующего от FrameworkElement через Decorator. Элемент Border разме- щается в центре плитки. Класс Decorator определяет (a Border наследует) свойст- во с именем Child, которое может содержать один экземпляр UIEIement; в нашем случае это TextBlock. Свойствам HorizontalAlignment и VerticalAlignment заданы зна- чения Center, чтобы текст находился по центру объекта Border. Объект Border мо- жет отобразить одноцветную границу (с закругленными краями), но класс Tile эту возможность не использует. Класс Tile предназначен для реализации головоломки «15». Вероятно, голово- ломка была изобретена в 1870-х годах американским изобретателем Сэмом Ллой- дом (1841—1911). В классическом варианте игровое поле состоит из 15 пронумеро- ванных плиток, расположенных в виде решетки 4x4; одна ячейка остается пустой, чтобы плитки можно было перемещать. Класс пустой ячейки выглядит так. Empty.cs //-------------------------------------- // Empty.cs (с) 2006 by Charles Petzold //-------------------------------------- namespace Petzol d.PlayJeuDeTacquin class Empty : System.Windows.FrameworkElement { } } В первоначальном варианте головоломки нумерованные плитки располага- лись по порядку, но квадраты 14 и 15 были переставлены. Сэм Ллойд предлагал $1000 любому, кто найдет способ перестановки плиток в правильном порядке. Как было впервые доказано в «American Journal of Mathematics» за 1879 год, ре- шения не существует. Задача также проанализирована в томе 4 «The World of Mathematics» (Simon and Schuster, 1956). В моей версии программы макет окна начинается с панели StackPanel с двумя дочерними объектами: кнопкой перемешивания плиток и объектом Border. Объект Border включен исключительно по эстетическим соображениям: он рисует тон- кую линию, отделяющую кнопку от содержимого Border. Свойство Child объекта Border содержит панель UniformGrid с 15 объектами Tile и одним объектом Empty.
146 Глава?. Canvas PlayJeuDeTacquin.es // // PlayJeuDeTacqu1n.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.WIndows.Controls.Pr1mlt1ves; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; namespace Petzold.PlayJeuDeTacquIn public class PlayJeuDeTacquIn ; Window { const Int NumberRows = 4; const Int NumberCols = 4; UniformGrid unigrid; Int xEmpty, yEmpty. 1 Counter; Key[] keys = { Key.Left, Key.Right, Key.Up, Key.Down }; Random rand: UIElement elEmptySpare = new EmptyO; [STAThread] public static void MalnO { Application app = new Appl1cat1on(); app.Run(new PlayJeuDeTacquIn()); } public PlayJeuDeTacqu1n() { Title = "Jeu de Tacquln": SizeToContent = SizeToContent.WidthAndHeight; ResIzeMode = ResIzeMode.CanMImmlze; Background = SystemColors.Control Brush; // Создание объекта StackPanel как содержимого окна StackPanel stack = new StackPanel0; Content = stack: // Создание кнопки в верхней части окна Button btn = new ButtonO: btn.Content = "-Scramble"; btn.Margin = new Thlckness(lO): btn.HorizontalAlignment = HorizontalAlignment.Center: btn. Click += ScrambleOnCUck;
147 Canvas stack.Children.Add(btn); // Создание декоративного объекта Border Border bord = new BorderO; bord.BorderBrush = SystemColors.ControlDarkDarkBrush; bord.BorderThlckness = new Thlckness(l): stack.Children.Add(bord); // Создание Uni formGrid как дочернего объекта Border unigrid = new Un1formGr1d(); uni grid.Rows = NumberRows; unigrid.Columns = NumberCols; bord.Child = unigrid; // Создание объектов Tile для заполнения // всех ячеек, кроме одной for (Int 1 = 0; 1 < NumberRows * NumberCols - 1; 1++) { Tile tile = new TlleO; tile.Text = (1 + 1).ToStr1ng(); tlle.MouseLeftButtonDown += TIleOnMouseLeftButtonDown; unigrid.Chi 1dren.Add(tile); } // Создание объекта Empty для заполнения последней ячейки unigrid.Children.Add(new EmptyO); xEmpty = NumberCols - 1; yEmpty = NumberRows - 1; } void T11e0nMouseLeftButtonDown(object sender, MouseButtonEventArgs args) Tile tile = sender as Tile; Int IMove = unigrid.Children.IndexOf(tlle); Int xMove = IMove % NumberCols; Int yMove = IMove / NumberCols; If (xMove == xEmpty) while (yMove != yEmpty) MoveT11e(xMove, yEmpty + (yMove - yEmpty) / Math.Abs(yMove - yEmpty)); If (yMove == yEmpty) while (xMove != xEmpty) MoveT11e(xEmpty + (xMove - xEmpty) / Math.Abs(xMove - xEmpty), yMove); } protected override void OnKeyDown(KeyEventArgs args)
148 Глава?. Canvas base.OnKeyDown(args); switch (args.Key) { case Key.Right: MoveT11e(xEmpty - 1. yEmpty); break case Key.Left: MoveTile(xEmpty + 1. yEmpty): break: case Key.Down: MoveTile(xEmpty. yEmpty - 1): break: case Key.Up: MoveTile(xEmpty. yEmpty +1): break: } } void ScrambleOnClick(object sender. RoutedEventArgs args) { rand = new RandomO; 1 Counter = 16 * NumberCols * NumberRows; DispatcherTimer tmr = new DIspatcherTImerO; tmr.Interval = TlmeSpan.FromMilliseconds(lO); tmr.TIck += TlmerOnTIck; tmr.Start 0: } void TimerOnTick(object sender, EventArgs args) { for (int i = 0; i < 5: i++) { MoveTile(xEmpty, yEmpty + rand.Next(3) - 1); MoveTile(xEmpty + rand.Next(3) - 1, yEmpty); } If (0 == ICounter--) (sender as DispatcherTimer) .StopO ; } void MoveTile(int xTile, int yTile) { If ((xTile == xEmpty && yTile == yEmpty) || xTile < 0 || xTile >= NumberCols || yTile < 0 || yTile >= NumberRows) return; Int ITIle = NumberCols * yTile + xTile; Int 1 Empty = NumberCols * yEmpty + xEmpty; UI Element elTIle = unigrid.Chi 1dren[1TI1e]; UIElement elEmpty = un1gnd.Ch11dren[1Empty'J; uni grid.Children.RemoveAt(ITIle); unlgrld.Chlldren.Insert(ITIle, elEmptySpare): unigrid.Chi 1dren.RemoveAt(1 Empty); unlgrld.Chlldren. InsertdEmpty, el Tile); xEmpty = xTile; yEmpty = yTile;
149 Canvas elEmptySpare = el Empty; } } } Программа реализует перемещения объектов Tile, выполняя операции с кол- лекцией Children объекта UniformGrid; это делается в методе MoveTile в самом кон- це файла. Два аргумента MoveTile содержат горизонтальную и вертикальную координа- ты перемещаемой плитки в решетке. Никакой неоднозначности в перемещении плитки нет — плитка может перемещаться только в ячейку, в настоящий момент занятую объектом Empty. Обратите внимание: код MoveTile сначала получает два переставляемых элемента индексированием коллекции Chidren, а затем меняет их местами парой вызовов RemoveAt и Insert. При выполнении подобных пере- становок необходима осмотрительность: при каждом удалении/вставке дочерне- го элемента индексы всех следующих за ним элементов изменяются. Управлять программой можно как с клавиатуры, так и мышью. Интерфейс мыши представлен обработчиком события MouseLeftButtonDown объекта Tile. Объ- ект Tile, на котором щелкнул пользователь, легко определяется преобразованием первого аргумента. Чтобы пользователь мог переместить несколько плиток од- ним щелчком, в программу включен цикл while. Клавиатурный интерфейс пред- ставлен переопределением метода OnKeyDown окна — клавиши со стрелками пе- ремещают в пустую ячейку плитку, прилегающую к ней. Количество строк и столбцов решетки в программе задается двумя полями. Размер игрового поля можно выбрать произвольно; впрочем, при четырехзнач- ных значениях становится трудно разобрать номера на плитках, а операция пе- ремешивания займет немало времени.
Зависимые свойства Следующая программа содержит шесть кнопок, объединенных в панель Grid из двух строк и трех столбцов. Каждая кнопка задает свойству FontSize указанное на ней значение. Однако три кнопки в верхней строке изменяют свойство FontSize окна, а три кнопки в нижней строке изменяют свойство FontSize кнопки, на ко- торой щелкнул пользователь. SetFontSizeProperty.cs // // SetFontS1zeProperty.cs (с) 2006 by Charles Petzold // using System: using System.Windows: using System.Windows.Controls: using System.Windows.Documents: using System.Windows.Input: using System.Windows.Media: namespace Petzold.SetFontSIzeProperty { public class SetFontSIzeProperty : Window [STAThread] public static void MalnO { Application app = new ApplicationO: app.Run(new SetFontSIzeProperty()); public SetFontS1zeProperty() { Title = "Set FontSize Property": SizeToContent = SIzeToContent.WldthAndHelght: ResizeMode = ResizeMode.CanMInlmlze; FontSize = 16: double[] fntslzes = { 8. 16. 32 }:
зависимые свойства 151 // Создание панели Grid Grid grid = new GridO; Content = grid; // Определение строк и столбцов for (int i = 0; i < 2; 1++) { RowDefinition row = new RowDefinitionO; row.Height = GridLength.Auto; grid.RowDef1nitions.Add(row); } for (Int 1 = 0; 1 < fntslzes.Length; 1++) ColumnDefinition col = new ColumnDef1n1t1on(); col.Width = GridLength.Auto; grid.ColumnDefinitions.Add(col); // Создание 6 кнопок for (Int 1 = 0; 1 < fntslzes.Length; 1++) Button btn = new ButtonO; btn.Content = new TextBlock( new Run("Set window FontSize to " + fntslzes[1])); btn.Tag = fnts1zes[1]; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center: btn. Click += WlndowFontSIzeOnCUck; grid.Children.Add(btn); Gr1d.SetRow(btn, 0); Grid.SetColumn(btn, 1); btn = new ButtonO; btn.Content = new TextBlock( new Run("Set button FontSize to " + fntsizesEU)); btn.Tag = fnts1zes[1]; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn. Click += ButtonFontSIzeOnCUck; grid.Children.Add(btn); Gr1d.SetRow(btn, 1); Grid.SetColumn(btn, 1); } } void W1ndowFontS1ze0nC11ck(object sender, RoutedEventArgs args) { Button btn = args.Source as Button; FontSize = (double)btn.Tag;
152 Глава 8. Зависимые свойства } void ButtonFontSIzeOnCl1ck(object sender. RoutedEventArgs args) { Button btn = args.Source as Button: btn.FontSize = (double)btn.Tag; > } Конструктор окна инициалнзнрует свойство FontSize 16 аппаратно-независи- мыми единицами. Окно создает панель Grid и заполняет ее 6 кнопками. На всех кнопках отображаются объекты TextBlock. Три кнопки в верхней строке задают свойство FontSize объекта Window равным 8, 16 или 32 единицам. Три кнопки в нижней строке изменяют свойство FontSize конкретной нажатой кнопки. После запуска программы размер текста на всех кнопках равен 16 единицам. Начальное значение FontSize, заданное для окна, «проходит» через панель Grid и кнопки и влияет на элемент TextBlock. Пощелкайте на кнопках в верхней стро- ке, и вы увидите, что все кнопки изменяются в соответствии с новым значением FontSize, заданным для окна. Но если щелкнуть на кнопке в нижней строке, изменится только эта кноп- ка — ведь обработчик задает свойство FontSize только для кнопки, на которой был сделан щелчок. Теперь снова щелкните на кнопке в верхнем ряду. Все кнопки, для которых значение FontSize было задано кнопками из нижней строки, перестают реагировать на изменение FontSize окна. Похоже, значение FontSize, за- данное для объекта Button, обладает более высоким приоритетом, чем значение, унаследованное объектом Button от Window. Window, Grid, шесть объектов Button и шесть объектов TextBlock образуют иерар- хию, которую можно представить примерно так. | Window |‘ | Grid | | Button | | Button | | Button | | Button | | Button | | Button | | TextBlock | | TextBlock | | TextBlock | | TextBlock | | TextBlock | | TextBlock | В иерархию входят все визуальные объекты, явно созданные в программе. Под «визуальным объектом» подразумевается объект с потенциальной возмож- ностью отображения на экране. Например, объект TextBlock, не содержащий тек- ста, или объект Grid без дочерних объектов все равно считаются визуальными объектами. Если говорить более формально, речь идет об объектах классов, на- следующих от класса Visual — обычно через классы UIEIement и FrameworkElement. Иерархия, показанная на рисунке, не является логическим деревом из доку- ментации WPF. В нашей программе логическое дерево также включает объ-
Зависимые свойства 153 екты RowDefinition и ColumnDefinition, связанные с Grid, и объекты Run, связанные с TextBlock. Эти объекты не являются визуальными, а их классы наследуют от ContentElement через FrameworkContentElement. Эта иерархия не является и тем, что называется в документации WPF визу- альным деревом, хотя и близка к последнему. Визуальное дерево может содер- жать визуальные объекты, которые не были явно созданы программой. Напри- мер, для отображения своей границы и ([зона объект Button создает объект типа ButtonChrome (определяемый в пространстве имен Microsoft.Windows.Themes), на- следующий от Visual, UIEIement и FrameworkElement через класс Decorator. Объекты типа ButtonChrome являются частью визуального дерева. Вероятно, есть и другие объекты, о которых мы даже не подозреваем. И все же дерево, изображенное на рисунке, поможет нам разобраться в ра- боте таких свойств, как FontSize. Когда программа явно задает свойство FontSize объекта визуального дерева, все объекты, расположенные ниже него, получают такое же значение FontSize. Говорят, что объекты на нижних уровнях дерева на- следуют свойство от родителей (не путайте этот термин с концепцией наследо- ванием классов). Но если свойство FontSize уже было явно задано для объекта, оно исключается из процесса наследования. Итак, когда вы щелкаете на одной из кнопок в верхней строке, обработчик события программы задает свойство FontSize окна. Очевидно, при этом незамет- но выполняется некая дополнительная работа, в результате которой значение FontSize задается всем потомкам окна — кроме тех, которым свойство FontSize было задано явно. У происходящего есть один интересный аспект: класс Grid не имеет свойства FontSize. Получается, что распространение свойства FontSize по дереву не сводит- ся к простой передаче от родителя к потомку. Объект Grid передает своим дочер- ним элементам новое значение FontSize, хотя сам он таким свойством не обладает. Когда вы щелкаете на одной из нижних кнопок, обработчик события задает свойство FontSize объекта Button, которое затем передается объекту TextBlock. По- пробуйте вставить в цикл for (после задания каждого свойства Content) две стро- ки вида (btn.Content as TextBlock).FontSize = 12; Программа перестает работать. После того как объект TextBlock получает явно заданное значение FontSize, значения, передаваемые посредством наследо- вания, отвергаются. Если свойство FontSize вообще не задается явно в программе, используется значение по умолчанию (оно равно 11 единицам). Можно сформулировать ряд обобщенных утверждений. Свойство FontSize имеет значение по умолчанию, но последнее обладает низким приоритетом. Зна- чение, унаследованное от предка в дереве элементов, обладает более высоким приоритетом, тогда как значение, явно заданное для объекта, обладает наивыс- шим приоритетом. FontSize — не единственное свойство, которое работает подобным образом. Класс UIEIement определяет четыре свойства (AllowDrop, IsEnabled, IsVisibie и Snap- ToDevicePixels), наследуемых по дереву элементов. Свойства Cultureinfo, FlowDirection
154 Глава 8. Зависимые свойства и InputScope, определяемые в классе FrameworkElement, тоже передаются по дереву, как и свойства FontFamily, FontSize, FontStretch, Fontstyles, FontWeight и Foreground, определяемые классом Control. На секунду представьте, что вы проектируете систему вроде WPF и хотите реализовать механизм наследования свойств по дереву элементов. Вероятно, вы постараетесь сделать его логически целостным и при этом свести к минимуму объем дублируемого кода. Щелкая на кнопках в программе SetFontSizeProperty, мы видим, как их раз- меры динамически изменяются в соответствии с новыми значениями FontSize, а изменение размеров кнопок также отражается на Grid и Window. Разумеется, при изменении свойства FontSize объекту Grid приходится пересматривать свой исход- ный макет. Вероятно, вы в роли проектировщика WPF сочтете, что изменения FontFamily и других шрифтовых свойств должны влиять на программу аналогич- ным образом. С другой стороны, при изменении свойства Foreground объект Grid не должен пересматривать свой макет; достаточно лишь перерисовать кнопки. В предыдущих главах вы узнали, как заставить экземпляры таких классов, как Brush и Shapes, динамически реагировать на изменения свойств. Данная кон- цепция заложена в систему анимации WPF. В главе 4 было показано, как свя- зать кнопку со свойством окна, а в следующих главах книги вы узнаете, что свойства объектов также могут связываться с шаблонами и стилями. Вероятно, при проектировании системы уровня WPF следует обеспечить логически цело- стный механизм реализации всех этих возможностей. Так мы приходим к концепции зависимых свойств — названных так потому, что они зависят от ряда других свойств и внешних факторов, причем порой весьма неочевидным образом. В традиционном программировании .NET размер шрифта объекта хранился бы в приватном поле (возможно, инициализированном значением по умолчанию): double fntsize = И; Внешний доступ к приватному полю осуществлялся бы через свойство FontSize: public double FontSize { get { return fntsize: } set { fntsize = value; } } Многоточие в методе доступа set означает, что здесь выполняется дополнитель- ный код. Возможно, элемент изменит свой размер, или по крайней мере, перери- сует себя на экране. А может быть, здесь инициируется событие FontSizeChanged или составляется список потомков в дереве элемента для наследования FontSize.
Зависимые свойства 155 В WPF механизм зависимых свойств обеспечивает автоматическое выполне- ние большей части работы по оповещению в стандартных ситуациях. Класс Control определяет свойство FontSize типа double, но наряду с ним также определяется ассоциированное поле FontSizeProperty типа Dependencyproperty: public class Control: FrameworkElement { public static readonly Dependencyproperty FontSizeProperty; } Это поле называется зависимым свойством. Оно является открытым и стати- ческим, то есть при обращении к нему вместо имени объекта указывается имя класса. Статические поля, доступные только для чтения, инициализируются .либо в определении самого поля, либо в статическом конструкторе. Обычно класс со- здает объект типа Dependencyproperty вызовом статического метода Dependency- Property. Register, который выглядит примерно так: FontSizeProperty = Dependencyproperty.Register("FontSize". typeof(double). typeof(Control)): В аргументах передается текстовое имя, ассоциируемое с зависимым свой- ством, тип данных свойства и тип класса, регистрирующего свойство. Впрочем, на практике минимального кода оказывается недостаточно — объект Dependency- Property часто содержит метаданные, описывающие важные аспекты свойства. Вероятно, следующий фрагмент больше напоминает регистрацию FontSizeProperty в классе Control: public class Control: FrameworkElement public static readonly Dependencyproperty FontSizeProperty: static Control 0 FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(): metadata.Defaultvalue = 11: metadata.AffectsMeasure = true: metadata.Inherits = true: metadata.IsDataBindingAllowed = true: metadata.DefaultUpdateSourceT ri gger = UpdateSourceT ri gger.PropertyChanged: FontSizeProperty = Dependencyproperty.Register("FontSize", typeof(double), typeof(Control). metadata. ValidateFontSize): } static bool ValidateFontSize(object obj)
156 Глава 8. Зависимые свойства { double dFontSize = (double) obj: return dFontSize > 0 && dFontSize <= 35791; } } В метаданных указывается значение по умолчанию И. Свойство FontSize влияет на размер элемента, поэтому свойство AffectMeasure задается равным true. Значение свойства (как мы уже видели) также наследуется по дереву элемен- тов — данная характеристика определяется свойством Inherits. Для него раз- решена привязка данных, а свойство DefaultUpdateSourceTrigger указывает, как именно она должна работать. (Привязка данных подробнее рассматривается в главе 23.) Кроме того, при вызове Dependency Property. Register в аргументах пе- редаются метаданные и метод, предоставляемый классом Control для проверки значений FontSize. Метод возвращает true, если значение положительно, но мень- ше некоторой максимальной величины. (Эксперименты показывают, что для свойства FontSize установлен верхний предел в 35 791 логическую единицу.) Вся подготовительная работа вступает в силу при определении свойства FontSize. Вероятно, в классе Control это делается примерно так: public class Control: FrameworkElement { public double FontSize { set { SetValue(FontSizeProperty. value); } get { return (double) GetValue(FontSizeProperty); } } } Откуда взялись методы SetValue и GetValue? Они определяются в классе Depen- dencyObject, который является предком многих классов WPF: Object Dispatcherobject (абстрактный) Dependencyobject Visual (абстрактный) UIEIement FrameworkElement Control
Зависимые свойства 157 Не путайте похожие имена: и Dependencyobject, и Dependencyproperty являются классами. Многие классы WPF наследуют от Dependencyobject, а следовательно, облада- ют методами SetValue и GetValue. Методы работают с полями, определяемыми как статические объекты Dependencyproperty. Несмотря иа то что объект Dependency- Property, передаваемый методам SetValue и GetValue, является статическим, сами методы SetValue и GetValue являются методами экземпляров и используются для чтения и записи данных конкретного экземпляра. Объект Dependencyobject под- держивает текущее значение, а также решает вспомогательные задачи. Напри- мер, если метод SetValue еще ие вызывался для некоторого экземпляра Control, GetValue возвращает значение свойства Defaultvalue, связанного с метаданными FontSizeProperty. Когда в программе задается свойство FontSize, метод SetValue должен выпол- нить кое-какую работу. Сначала он должен выполнить метод проверки, чтобы определить задаваемое значение. Если значение недопустимо, метод выдает ис- ключение. Поскольку для свойства FontSizeProperty установлен флаг AffectsMeasure, метод SertValue должен пересчитать размер элемента, а для этого также потребует- ся перерисовать элемент. Логика перерисовки использует новое значение FontSize для вывода текста. (Для таких свойств, как Foreground, флаг AffectsMeasure равен false, а флаг AffectsRender равен true — элемент не изменяется в размерах, ио пе- рерисовывается.) Метод SetValue также должен передать повое значение вниз по дереву элементов, на что указывает флаг Inherits. Элементы дерева могут при- нять новое значение или отвергнуть его, если их свойство FontSize было задано явно. Конечно, анализ работы FontSize помогает понять, как работают зависимые свойства, но теоретический материал по-настоящему усваивается лишь тогда, когда вы займетесь практическим определением зависимых свойств. Следующий класс наследует от Button, но добавляет к нему новое зависимое свойство. Класс называется SpaceButton и предназначается для отображения тек- ста, разделенного пробелами. К свойствам обычного класса Button добавляются два новых свойства: Text и Space. Для большей наглядности класс реализует Text как традиционное свойство .NET, а свойство Space реализуется с использовани- ем зависимого свойства. Объект SpaceButton задает своему свойству Content со- держимое строки Text, разделяемое Space-пробелами. SpaceButton.cs //-------------------------------------------- // SpaceButton.cs (с) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Text: using System.Windows: using System.Windows.Controls: using System.Windows.Input; using System.Windows.Media:
158 Глава 8. Зависимые свойства namespace Petzold.SetSpaceProperty { public class SpaceButton : Button { // Традиционное приватное поле .NET и открытое свойство string txt; public string Text { set { txt = value; Content = SpaceOutText(txt); } get { return txt; } } // Dependencyproperty и открытое свойство public static readonly Dependencyproperty SpaceProperty; public Int Space { set { SetValue(SpaceProperty, value); } get { return (Int)GetValue(SpaceProperty); } } // Статический конструктор static SpaceButtonO { // Определение метаданных FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(); metadata.Defaultvalue = 1; metadata.AffectsMeasure = true; metadata.Inherits = true; metadata.PropertyChangedCallback += OnSpacePropertyChanged; // Регистрация Dependencyproperty SpaceProperty =
Зависимые свойства 159 Dependencyproperty.Register("Space". typeof(int). typeof(SpaceButton). metadata. VaiidateSpaceValue): } // Метод обратного вызова для проверки значения static bool VaiidateSpaceValue(object obj) { Int 1 = (int)obj: return 1 >= 0: } // Метод обратного вызова для оповещения об изменении свойства static void OnSpacePropertyChanged(DependencyObject obj. DependencyPropertyChangedEventArgs args) { SpaceButton btn = obj as SpaceButton; btn.Content = btn.SpaceOutText(btn.txt); } // Метод для вставки пробелов в текст string SpaceOutText(string str) { if (str == null) return null: StringBuilder build = new StringBuilderO: foreach (char ch in str) build. Append (ch + new stringU Space)); return build.ToStringO; } } } Программа начинается с традиционной реализации свойства Text в виде при- ватного поля с именем txt. У традиционных свойств .NET в метод доступа set обычно включается код, который делает что-то полезное с новым свойством. В нашей программе свойство Text вызывает метод SpaceOutText для вставки про- белов в текст, а затем задает полученный результат свойству Content. Оставшаяся часть файла связана с реализацией свойства Space. Класс опре- деляет открытое статическое поле SpaceProperty, доступное только для чтения, и реализует методы доступа set и get свойства Space с использованием вызовов методов SetValue и GetValue, определенных в классе Dependencyobject. Статический конструктор класса готовит метаданные и регистрирует зави- симое свойство. Обратите внимание на два метода обратного вызова. Свойство ValldateSpaceValue возвращает true, если значение является допустимым. Отрица- тельное количество пробелов не имеет смысла, поэтому для отрицательных ве- личин ValldateSpaceValue возвращает false.
160 Глава 8. Зависимые свойства Метод OnSpacePropertyChanged вызывается при каждом изменении свойства. Как и метод доступа set для свойства Text, этот метод задает свойство Content кнопки по возвращаемому значению SpaceOutText. Метод SpaceOutText обращает- ся к свойству Space для получения нужного количества пробелов. Оба метода обратного вызова должны определяться как статические. Для ме- тода ValidateSpaceValue это не создает особых проблем, но метод OnSpaceProperty- Changed должен преобразовать первый аргумент к типу SpaceButton и использо- вать его во всех ссылках на члены объекта. Но фактически он делает то же самое, что и метод доступа set свойства Text. Обратите внимание: класс SpaceButton задает свойствам AffectsMeasure и Inherits метаданных значение true, но не содержит дополнительного кода для реализа- ции этих функций. Все происходит автоматически. Ранее упоминавшееся свойство FontSize определяется в классе Control и насле- дуется классами Button и Window. Новое свойство Space, добавленное в Button, долж- но наследоваться по дереву элементов, но нигде больше не определяется. Чтобы продемонстрировать наследование свойства, давайте создадим класс с именем SpaceWindow, реализующий это свойство. Класс SpaceWindow это свойство никак не использует, поэтому он получается заметно короче класса SpaceButton. SpaceWindow.cs // // SpaceWindow.cs (с) 2006 by Charles Petzold // using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: namespace Petzold.SetSpaceProperty { public class SpaceWindow : Window { // Dependencyproperty и свойство public static readonly Dependencyproperty SpaceProperty: public int Space { set { SetValue(SpaceProperty. value); } get { return (int)GetValue(SpaceProperty); } } // Статический конструктор static SpaceWindow()
Зависимые свойства 161 { // Определение метаданных FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(); metadata.Inherits = true; // Добавление владельца к SpaceProperty // и переопределение метаданных SpaceProperty = SpaceButton.SpaceProperty.AddOwner(typeof(SpaceWindow)); SpaceProperty.OverrideMetadata(typeof(SpaceWindow), metadata); } } } Как и SpaceButton, этот класс определяет поле SpaceProperty и само свойство Space. Однако статический конструктор не регистрирует новое свойство Space, а добавляет нового владельца свойства Space, уже зарегистрированного классом SpaceButton. Когда класс добавляет нового владельца к ранее зарегистрированно- му зависимому свойству, исходные метаданные не применяются, и класс дол- жен создать собственные метаданные. Так как класс SpaceWindow почти ничего не делает со свойством Space, ему необходимо позаботиться лишь об установке флага Inherits. Последний класс проекта — SetSpaceProperty — очень похож на SetFontSize- Property. SetSpacePropeity.es //------------------------------------------------- // SetSpaceProperty.cs (с) 2006 by Charles Petzold //------------------------------------------------- using System: using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.SetSpaceProperty { public class SetSpaceProperty : SpaceWindow { [STAThread] public static void MainO { Application app = new ApplicationO: app. Run (new SetSpacePropertyO): public SetSpacePropertyO { Title = "Set Space Property"; SizeToContent = SizeToContent.WidthAndHeight;
162 Глава 8. Зависимые свойства ResizeMode = ResizeMode.CanMinimize; int[] iSpaces = { 0, 1, 2 }; Grid grid = new Grid(); Content = grid: for (int i = 0; i < 2: i++) { RowDefinition row = new RowDefinitionO: row.Height = GridLength.Auto: grid.RowDefi ni ti ons.Add(row); } for (int i = 0: i < iSpaces.Length; 1++) { ColumnDefinition col = new ColumnDefinition(): col.Width = GridLength.Auto; grid.ColumnDefinitions.Add(col); } for (int i = 0: i < iSpaces.Length: i++) { SpaceButton btn = new SpaceButton(); btn.Text = "Set window Space to " + iSpaces[i]: btn.Tag = iSpacesLi1; btn.HorizontalAlignment = HorizontalAlignment.Center: btn.VerticalAlignment = VerticalAlignment.Center: btn.Click += WindowPropertyOnClick; grid.Children.Add(btn): Grid.SetRow(btn, 0): Grid.SetColumn(btn, i): btn = new SpaceButton0; btn.Text = "Set button Space to " + ISpacesLi3: btn.Tag = iSpacesLi]: btn.HorizontalAlignment = HorizontalAlignment.Center: btn.Vertical Alignment = VerticalAlignment.Center: btn.Click += ButtonPropertyOnClick; grid.Children.Add(btn); Grid.SetRow(btn, 1); Grid.SetColumn(btn, i): } } void WindowPropertyOnClick(object sender, RoutedEventArgs args) { SpaceButton btn = args.Source as SpaceButton: Space = (Int)btn.Tag; } void ButtonPropertyOnClick(object sender, RoutedEventArgs args) { SpaceButton btn = args.Source as SpaceButton:
Зависимые свойства 163 btn.Space = (int)btn.Tag; } } } Обратите внимание: класс является производным от SpaceWindow, а не от Window. Как и в предыдущей программе, он создает шесть кнопок, но эти кноп- ки относятся к типу SpaceButton. Символы на кнопках разделяются одним про- белом, потому что это значение используется по умолчанию. Как и в предыду- щей программе, щелчок на кнопке изменяет все кнопки нижней строки, но при щелчке на кнопке нижней строки изменяется только эта кнопка — и остается неизменной в дальнейшем. Благодаря реализации зависимого свойства класс SpaceButton готов к использованию привязки данных, стилей, анимации и про- чих визуальных излишеств. Настало время раскрыть еще одну тайну. В главе 5 был представлен класс DockPanel со статическими методами SetDock и GetDock; похожие свойства также существуют у классов Grid и Canvas. Синтаксис SetDock выглядит довольно странно: DockPanel.SetDock(Ctrl, Dock.Right): Также можно было пойти по другому пути и определить в UIEIement свойство Dock, которое бы задавалось следующим образом: Ctrl.Dock = Dock.Right: // В WPF так не делается Так задача решается в Windows Forms. Но в WPF свойство Dock вступает в иг- ру только в том случае, если элемент является дочерним по отношению к Dock- Panel. Но могут существовать п другие панели (даже определенные вами), для которых потребуются другие свойства; нет смысла обременять UIEIement много- численными свойствами для разных типов панелей. Другая возможность — создание расширенного метода Add свойства Children: dock.Children.Add(ctrl, Dock.Right): // В WPF так не делается Но свойство Children в действительности является объектом типа UIEIement- Collection и используется для всех панелей, а не только для Dock.Panel. В еще од- ном решении задействовано свойство экземпляра DockPanel с именем SetDock: dock.SetDock(ctrl, Dock.Right): // В WPF так не делается Конечно, и этот вариант работает, но DockPanel придется поддерживать вто- рую коллекцию элементов с ассоциированными значениями Dock. При построе- нии макета DockPanel будет перебирать все элементы коллекции Children, а затем искать во второй коллекции совпадающий элемент для определения значения Dock. Казалось бы, реально используемый синтаксис DockPanel.SetDock(Ctrl, Dock.Right): подразумевает хранение элемента и связанного с ним значения Dock в коллекции. Но в действительности свойства SetDock и GetDock класса DockPanel реализуются так: public class DockPanel : Panel
164 Глава 8. Зависимые свойства public static readonly Dependencyproperty DockProperty; public static void SetDock(UIEIement el, Dock dck) { el.SetValue(DockProperty, dck); } public static Dock GetDock(UIEIement el) { return (Dock) el.GetValue(DockProperty): } } Свойство DockProperty определяется с типом Dependencyproperty, но регистри- руется методом DependencyProperty.RegisterAttached и называется присоединенным свойством (attached property). Если бы оно было обычным зависимым свой- ством, то DockProperty ассоциировалось бы со свойством Dock вызовами SetValue и GetValue, однако свойства Dock не существует. Вместо этого для работы с Dock- Property используются два статических метода SetDock и GetDock, определяемых в классе DockPanel. Эти методы вызывают SetValue и GetValue для элемента, пере- даваемого в аргументе. Речь идет о тех же методах SetValue и GetValue, которые определяются в Dependencyobject и используются в сочетании с зависимыми свойствами. (Однако следует заметить, что класс, реализующий присоединен- ное свойство, не вызывает свои методы SetValue и GetValue. По этой причине класс, реализующий присоединенное свойство, не обязан быть производным от Dependencyobject.) Вызов SetDock в типичной программе выглядит примерно так: DockPanel.SetDock(Ctrl. Dock.Right): Он полностью эквивалентен следующему вызову: Ctrl.SetValue(DockPanel.DockProperty, Dock.Right): Чтобы убедиться в этом, достаточно заменить вызовы SetDock эквивалентны- ми вызовами SetValue. Вероятно, при получении вызова своего метода SetValue с аргументом DockPanel. DockProperty объект Ctrl сохраняет свойство и значение в некой коллекции. Но эта коллекция является частью дочернего элемента, а не частью объекта DockPanel. При размещении своих элементов объект DockPanel может получить значение Dock, связанное с элементом, вызовом статического метода GetDock: Dock dck = DockPanel.GetDock(Ctrl); Этот вызов эквивалентен следующему:. Dock dck = (Dock) Ctrl.GetValue(DockPanel.DockProperty); Присоединенные свойства встречаются гораздо реже обычных зависимых свойств, причем большинство самых важных примеров мы уже видели.
9 Маршрутизируемые события ввода WPF поддерживает три основных вида пользовательского ввода: с клавиату- ры, мыши и стилуса. (Ввод со стилуса используется на планшетных компьюте- рах и графических планшетах.) В предыдущих главах мы уже видели, как на- значаются обработчики событий клавиатуры и мыши, но в этой главе события ввода будут рассмотрены более подробно. События ввода определяются с использованием делегатов, вторым аргумен- том которых является тип, наследующий от RoutedEventArgs через InputEventArgs. Далее приводится полная иерархия классов от узла InputEventArgs: Object EventArgs RoutedEventArgs InputEventArgs KeyboardEventArgs KeyboardFocusChangedEventArgs KeyEventArgs MouseEventArgs MouseButtonEventArgs MouseWheelEventArgs QueryCursorEventArgs StylusEventArgs StylusButtonEventArgs StylusDownEventArgs StylusSystemGestureEventArgs TextComposi ti onEventArgs Класс RoutedEventArgs используется в WPF исключительно как часть поддерж- ки маршрутизации событий — механизма, благодаря которому элементы орга- низуют чрезвычайно гибкую обработку событий. Маршрутизацией событий на- зывается их перемещение вверх и вниз по дереву элементов. Пользователь щелкает кнопкой мыши. Кто получает событие? Традиционный ответ: «видимый незаблокированный элемент, находящийся на переднем пла- не под указателем мыши». Если пользователь щелкает на кнопке, находящейся в окне, то событие получит кнопка. Все очень просто.
166 Глава 9. Маршрутизируемые события ввода Но в WPF этот упрощенный подход не решает всех проблем. Кнопка облада- ет содержимым, которое может содержать панель, которая, в свою очередь, со- держит фигуры, изображения и текстовые блоки. Каждый из этих элементов способен получать события мыши. Иногда обработка событий мыши на уровне этих отдельных элементов имеет смысл — но так бывает не всегда. Если элемен- ты просто украшают поверхность кнопки, обработку событий логичнее пору- чить кнопке. Пользователь нажимает клавишу на клавиатуре. Кто получает событие? Тра- диционный ответ: «элемент, обладающий фокусом ввода». Например, если окно содержит несколько текстовых полей, только одно поле обладает фокусом вво- да; именно оно получит событие. Но подумайте, как удобно было бы иметь механизм, позволяющий окну за- ранее просматривать события клавиатуры? Возможно, окно само захочет обра- ботать событие, прежде чем оно будет передано текстовому полю. (Именно так действует программа EditSomeRichText в главе 4, вызывая диалоговые окна при нажатии клавиш Ctrl+O и Ctrl+S.) Маршрутизация событий в WPF делает возможными оба сценария. Всегда существует элемент, который считается «источником» события. Для событий мыши и стилуса источником обычно является элемент, находящийся на перед- нем плане под указателем мыши или стилуса. Этот элемент должен быть как видимым, так и незаблокированным. (Иначе говоря, свойство Visibility элемента должно быть равно Visibility.Visible, а свойство IsEnabled должно быть истинным.) Если это условие не выполняется, источником события является верхний види- мый и незаблокированный элемент из нижележащих элементов. Источником клавиатурного события является интерфейсный элемент, обла- дающий фокусом ввода. И для событий клавиатуры, и для событий мыши ис- точником любого конкретного события может быть только один элемент, но все предки элемента-источника по дереву элементов также могут получить доступ к событию. Большинство событий пользовательского ввода определяется в классе UIEIe- ment. В частности, в секции «События» документации UIEIement описано собы- тие MouseDown, сигнализирующее о щелчке кнопкой мыши. В секции «Методы» документации UIEIement присутствует OnMouseDown — защищенный виртуальный метод, соответствующий данному событию. Любой класс, производный от UIEIe- ment, может переопределить метод OnMouseDown вместо того, чтобы устанавли- вать обработчик события MouseDown. Эти два подхода функционально идентич- ны, но вызов OnMouseDown предшествует инициированию события MouseDown. В переопределениях виртуальных методов (таких, как OnMouseDown) обычно вызывается метод базового класса: protected override void OnMouseDown(MouseEventArgs args) { base.OnMouseDown(a rgs);
маршрутизируемые события ввода 167 В Windows Forms вызов метода OnMouseDown базового класса был очень ва- жен, потому что именно в нем базовый класс инициировал событие MouseDown. Реализация метода OnMouseDown в классе Control в Windows Forms выглядела примерно так: // Windows Forms, не WPF! protected virtual void OnMouseDown(MouseEventArgs args) { if (MouseDown != null) MouseDown(this, args) } Теоретически в Windows Forms класс MyControl может наследовать от Control и переопределять OnMouseDown без вызова метода базового класса. Обычно так поступать не рекомендуется — если программа затем создаст объект типа MyControl и назначит ему обработчик события MouseDown, этот обработчик не получит ни- каких событий. В WPF подобной проблемы не существует. Событие MouseDown инициирует- ся вне вызова OnMouseDown. Как указано в документации метода OnMouseDown класса UIEIement, «метод не имеет реализации по умолчанию». Иначе говоря, тело метода OnMouseDown в классе UIEIement остается пустым. В классе, производном прямо от UIEIement, можно переопределить OnMouseDown без вызова метода базо- вого класса, и это не приведет к нежелательным побочным эффектам для клас- сов, производных от вашего класса, или создающих его экземпляры. Аналогичное утверждение насчет отсутствия «реализации по умолчанию» также встречаются в описаниях других виртуальных методов UIEIement, относящихся к вводу. В документации WPF указаны не только определения виртуальных мето- дов классами, но и переопределения этих методов. Взгляните на список методов FrameworkElement — класса, напрямую наследующего от UIEIement. Видите в нем метод OnMouseDown? Нет, потому что FrameworkElement не переопределяет On- MouseDown. Теперь обратитесь к классу Control, производному непосредственно от FrameworkElement. И снова метода OnMouseDown пет в списке. Теперь обрати- тесь к классу ButtonBase. Здесь вы найдете переопределения не только метода OnMouseDown, но и OnMouseLeftButtonDown — сопутствующего метода, используе- мого классом ButtonBase для выдачи событий Click. Если класс наследует от ButtonBase и вы переопределяете OnMouseLeftButtonDown без вызова метода базо- вого класса, тем самым вы фактически блокируете событие Click. В описании ме- тода OnMouseLeftButtonDown класса ButtonBase не сказано, что метод «не имеет реализации по умолчанию». Хотя UIEIement не инициирует свои события в соответствующих методах On, другие классы могут выбрать другой путь. Например, класс ButtonBase опреде- ляет метод OnClick. Переопределение OnMouseLeftButtonDown в ButtonBase почти наверняка вызывает OnClick, а метод OnClick отвечает за выдачу события Click. Как и в случае с методом OnMouseLeftButtonDown, если класс наследует от ButtonBase,
168 Глава 9. Маршрутизируемые события ввода a OnClick переопределяется без вызова метода базового класса, это приводит к бло- кировке события Click для всех объектов, созданных на базе класса. В общем случае при переопределении виртуальных методов рекомендуется вызывать метод базового класса, если только у вас нет веских причин для об- ратного. Я предпочитаю вызывать метод базового класса даже тогда, когда в со- ответствии с документацией это делать не обязательно. К событию MouseDown и виртуальному защищенному методу OnMouseDown присоединяются другие члены класса UIEIement. Так, в секции «Поля» доку- ментации UIEIement также встречается статическое поле MouseDownEvent типа RoutedEvent, доступное только для чтения. Как будет показано далее, объекты RoutedEvent играют примерно такую же роль, как зависимые свойства. Mouse- DownEvent позволяет легко обращаться к событию MouseDown из некоторых вы- зовов методов, а в классе RoutedEvent инкапсулируется информация, управляю- щая передачей события другим элементам дерева. В документации UIEIement также описано событие PreviewMouseDown, метод OnPreviewMouseDown и поле PreviewMouseDownEvent. Данная схема выбора имен задействована в большинстве событий пользовательского ввода, определяемых классом UIEIement. Вскоре мы увидим, как работают эти две группы событий. Иерархия классов, представленная в начале главы, наводит на мысль, что класс RoutedEventArgs играет важную роль в обработке пользовательского ввода WPF. Кроме InputEventArgs, существует немало других классов, производных от RoutedEventArgs. Класс RoutedEventArgs определяет всего четыре свойства, часто используемых при обработке событий. Одно из этих свойств — RoutedEvent — является объек- том типа RoutedEvent. Свойство RoutedEvent класса RoutedEventArgs определяет само событие. Например, для события MouseDown оно равно MouseDownEvent — статическому полю, определяемому в UIEIement. Один обработчик может использоваться для разных типов событий. Код иден- тификации события выглядит примерно так: if (args.RoutedEvent == MouseDownEvent) { } else if (args.RoutedEvent == MousellpEvent) { } Свойства Source и OriginalSource класса RoutedEventArgs относятся к типу object; они определяют элемент, который является источником события. Обычно важ- ная информация содержится в свойстве Source, но в некоторых случаях также приходится проверять свойство OriginalSource. Свойство Handled класса RoutedEventArgs изначально ложно, по его можно за- дать равным true для предотвращения дальнейшей маршрутизации события. На- пример, программа EditSomeRichText в главе 4 использует его для предотвра- щения передачи нажатий клавиш Ctrl+O и Ctrl+S объекту RichTextBox.
Маршрутизируемые события ввода 169 К свойствам, определяемым RoutedEventArgs, класс InputEventArgs добавляет свойство Device типа InputDevice, идентифицирующее устройство ввода, а также свойство TimeStamp типа int. Рассмотрим реальный пример маршрутизации событий. Первое, на что нуж- но обратить внимание в следующей программе ExamineRoutedEvents, — то, что класс объявляется производным от Application, а не от Window, а затем создает объ- ект типа Window и несколько других элементов. В исходной версии программы определялся класс ExamineRoutedEvents, производный от Window, но как вы веко- ре увидите, в программе выводятся имена классов вида «TextBlock», «Button» и «Grid». Класс с именем «ExamineRoutedEvents» вместо простого «Window» выглядел слишком странно. ExamineRoutedEvents.cs // // ExamineRoutedEvents.es (с) 2006 by Charles Petzold // using System: using System.Windows: using System.Windows.Controls: using System.Windows.Documents; using System.Windows.Input: using System.Windows.Media; namespace Petzold.ExamineRoutedEvents { public class ExamineRoutedEvents: Application { static readonly FontFamily fontfam = new FontFamilyCLucida Console"); const string strFormat = "{0.-30} {1,-15} {2.-15} {3,-15}"; StackPanel stackoutput: DateTime dttast; [STAThread] public static void MainO { ExamineRoutedEvents app = new ExamineRoutedEventsO: app.RunO: } protected override void OnStartup(StartupEventArgs args) { base.OnStartup(args); // Создание окна Window win = new WindowO; . win.Title = "Examine Routed Events":
170 Глава 9. Маршрутизируемые события ввода // Создание объекта Grid и назначение его содержимым Window Grid grid = new GridO; win.Content = grid: // Определение трех строк RowDefinition rowdef = new RowDefinitionO: rowdef.Height = GridLength.Auto: grid.RowDefi n i t i ons.Add(rowdef): rowdef = new RowDefinitionO; rowdef.Height = GridLength.Auto: gri d.RowDefi ni ti ons.Add(rowdef); rowdef = new RowDefinitionO; rowdef.Height = new GridLength(100, GridUnitType.Star): grid.RowDefi ni ti ons.Add(rowdef): // Создание объекта Button и его размещение на панели Grid Button btn = new ButtonO: btn.Horizontal Alignment = HorizontalAlignment.Center: btn.Margin = new Thickness(24); btn.Padding = new Thickness(24): grid.Children.Add(btn); // Создание объекта TextBlock и его добавление к Button TextBlock text = new TextBlockО: text.FontSize = 24; text.Text = win.Title: btn.Content = text; // Создание заголовков для вывода над Scrollviewer TextBlock textHeadings = new TextBlockО: textHeadings.FontFamily = fontfam: textHeadings.Inlines.Add(new Underline(new Run( String.Format(strFormat, "Routed Event", "sender". "Source", "OriginalSource")))); grid.ChiIdren.Add(textHeadings): Grid.SetRow(textHeadings. 1); // Создание объекта Scroll Viewer Scroll Vi ewer scroll = new ScrollViewerO: grid.Chi 1dren.Add(scroll); Grid.SetRow(scroll, 2): // Создание объекта StackPanel для вывода событий stackOutput = new StackPanelО; scroll.Content = stackOutput:
МяршрУтизиРУемЬ|е со6ытия ввода 171 // Добавление обработчиков событий UIElernentE] els = { win, grid, btn. text }; foreach (UIEIement el in els) { // Клавиатура el.PreviewKeyDown += AllPurposeEventHandler; el.PreviewKeyUp += AllPurposeEventHandler; el.PreviewTextlnput += AllPurposeEventHandler; el.KeyDown += AllPurposeEventHandler; el.KeyUp += AllPurposeEventHandler; el.Textinput += AllPurposeEventHandler; // Мышь el.MouseDown += AliPurposeEventHandler; el.MouseUp += AllPurposeEventHandler; el.PreviewMouseDown += AlIPurposeEventHandler; el.PreviewMouseUp += AllPurposeEventHandler; // Стилус el.StylusDown += AllPurposeEventHandler; el.StylusUp += AllPurposeEventHandler; el.PreviewStylusDown += AllPurposeEventHandler; el.PreviewstylusUp += AllPurposeEventHandler; // Щелчок el.AddHandler(Button.Cli ckEvent, new RoutedEventHandler(Al 1PurposeEventHandler)); } // Отображение окна win.Show(); } void AllPurposeEventHandler(object sender. RoutedEventArgs args) { // Вывод пустой строки, если события разделены промежутком DateTime dtNow = DateTime.Now; if (dtNow - dtLast > TimeSpan.EromMilliseconds(lOO)) stackOutput.Children.Add(new TextBlock(new Run(" "))); dtLast = dtNow; // Вывод информации о событии TextBlock text = new TextBlockO; text.EontEamily = fontfam; text.Text = String.Eormat(strEormat, a rgs.RoutedEvent.Name, TypeWithoutNamespace(sender), TypeWithoutNamespace(args.Source), TypeWithoutNamespace(args.Original Source)); stackOutput.ChiIdren.Add(text);
172 Глава 9. Маршрутизируемые события ввода (stackOutput.Parent as Scrol 1 Vi ewer) .Scroll ToBottomO; string TypeWithoutNamespace(object obj) { string[] astr = obj.GetTypeO.ToStringO.SplitC .'): return astr[astr.Length - 1]; } } Программа создает интерфейсные элементы, объединенные в дерево следую- щего вида. | Window | | Gtid | | Button | | TextBlock | | Scrollviewer | | TextBlock | | StackPanel | | TextBlock... | Объект Window считается корневым узлом дерева. Все остальные элемен- ты считаются расположенными «ниже» корня, а для элементов TextBlock объект Window располагается «выше». Программа ExamineRoutedEvents устанавливает различные обработчики со- бытий для элементов Window, Grid и Button, а также для содержимого Button — объекта TextBlock. Эти обработчики относятся к разным событиями клавиату- ры, мыши и стилуса, но во всех случаях используется общий метод AHPurpose- EventHandler. Каждый раз, когда метод AIIPurposeEventHandler получает новое событие, он создает новый объект TextBlock (соответствующий объекту TextBlock с многото- чием на диаграмме). Объект TextBlock выводит информацию о событии, включая первый аргумент обработчика события (обычно называемый sender), а также свойства RoutedEvent, Source и OriginalSource аргумента RoutedEventArgs. Затем об- работчик события добавляет объект TextBlock на панель StackPanel, являющуюся содержимым Scrollviewer. Элемент TextBlock в центре диаграммы просто предо- ставляет заголовки для информации, выводимой программой. Чтобы выходные данные лучше читались, AIIPurposeEventHandler пытается вставлять пустые строки между событиями, разделенными интервалом более чем на 100 миллисекунд. Учтите, что эта возможность не всегда работает иде- ально — особенно если у вас очень быстрые пальцы, или события были отложе- ны по какой-то причине.
Маршрутизируемые события ввода 173 При запуске программы в верхней части окна располагается большая кнопка с надписью. Щелкните на тексте в кнопке правой кнопкой мыши. Это должно вызвать последовательность событий, перечисленных в следующей таблице. RoutedEvent sender Source OriginalSource PreviewMouseDown Window TextBlock TextBlock PreviewMouseDown Grid TextBlock TextBlock PreviewMouseDown Button TextBlock TextBlock PreviewMouseDown TextBlock TextBlock TextBlock MouseDown TextBlock TextBlock TextBlock MouseDown Button TextBlock TextBlock MouseDown Grid TextBlock TextBlock MouseDown Window TextBlock TextBlock За этой серией следует аналогичная цепочка событий PreviewMouseUp и MouseUp. (Если свойство OriginalSource равно ButtonChrome, значит, вы не попали по тексту внутри кнопки.) Первый аргумент обработчика события — обычно называемый sender — все- гда содержит объект, для которого был установлен обработчик. Он указывает объект, отправляющий событие приложению. Свойство Source объекта Routed- EventArgs определяет элемент, инициировавший событие. Последовательность событий, приведенная в таблице, отражает сущность мар- шрутизации. Событие PreviewMouseDown относится к категории опускающихся событий. Оно начинается с корня визуального дерева и постепенно «углубляет- ся» к элементу, расположенному непосредственно под указателем мыши. Собы- тие MouseDown является примером поднимающегося события — оно «всплывает» через цепочку визуальных родителей к корневому элементу. Механизм маршрутизации отличается чрезвычайной гибкостью. Если эле- мент, находящийся выше в дереве, захочет получить первоочередной доступ к нажатию кнопки мыши, он может сделать это посредством обработки события PreviewMouseDown. Если какой-либо из этих элементов интересуется только со- бытиями, проигнорированными элементами нижних уровней, он обрабатывает MouseDown. Теперь слегка отведите указатель от текста, но не выходя за пределы кнопки, и снова щелкните правой кнопкой мыши. Вот что вы увидите на этот раз. RoutedEvent sender Source OriginalSource PreviewMouseDown Window Button ButtonChrome PreviewMouseDown Grid Button ButtonChrome PreviewMouseDown Button Button ButtonChrome MouseDown Button Button ButtonChrome MouseDown Grid Button ButtonChrome MouseDown Window Button ButtonChrome
174 Глава 9. Маршрутизируемые события ввода Далее следует аналогичная серия для PreviewMousellp и MouseUp. Объект Text- Block в обработке события не участвует, потому что он не находится под указате- лем мыши. Согласно свойству Source, событие инициируется объектом Button, но свойство OriginalSource говорит о том, что объектом переднего плана, находящим- ся непосредственно под указателем мыши, был объект типа ButtonChrome. Слы- шали о таком? Я тоже не слышал, пока не написал и не запустил эту программу. Класс ButtonChrome наследует от Decorator (который, в свою очередь, наследует от FrameworkElement) и принадлежит пространству имен Microsoft.Windows.Themes. Класс Button использует его для прорисовки своей поверхности. Вывод: на свойство OriginalSource часто можно не обращать внимания и со- средоточиться на свойстве Source. (Впрочем, если Source не дает необходимой информации, приходится обращаться к OriginalSource.) В предшествующих инструкциях вам предлагалось щелкать правой кнопкой мыши, и это имеет объяснение. Если щелкнуть на TextBlock левой кнопкой мыши, вы увидите следующую последовательность событий. RoutedEvent sender Source OriginalSource PreviewMouseDown Window TextBlock TextBlock PreviewMouseDown Grid TextBlock TextBlock PreviewMouseDown Button TextBlock TextBlock PreviewMouseDown TextBlock TextBlock TextBlock MouseDown TextBlock TextBlock TextBlock И на этом серия событий MouseDown завершается. Что произошло? Оказывается, класс Button особо интересуется левой кнопкой мыши, потому что именно она генерирует события Click. Как упоминалось ранее, ButtonBase пе- реопределяет метод OnMouseLeftButtonDown. При вызове этого метода ButtonBase начинает работу, которая в конечном счете приведет к созданию события Click. Так как ButtonBase фактически обрабатывает левую кнопку мыши, флагу Handled объекта RoutedEventArgs задается истинное значение. Флаг прерывает передачу вызова обработчикам событий MouseDown и дальнейшую маршрутизацию собы- тия по дереву. При отпускании левой кнопки мыши выдается следующая последователь- ность событий. RoutedEvent sender Source OriginalSource PreviewMousellp Window Button Button PreviewMousellp Grid Button Button PreviewMousellp Button Button Button Click Button Button Button Click Grid Button Button Click Window Button Button
Маршрутизируемые события ввода 175 Объект TextBlock уже не участвует в обработке. Объект Button «захватил мышь» (об этом чуть позже) и стал источником события. Во время вызова метода OnLeft- MouseButtonUp класс ButtonBase устанавливает флаг Handled и генерирует событие СЦск, которое продолжает передаваться по дереву окну. (Событие Click существует только в «поднимающемся» варианте, без «опускающегося» события PreviewClick.) Событие Click определяется классом ButtonBase и наследуется Button. Ни класс Window, ни Grid ничего не знают ни о событиях Click, поэтому установить обра- ботчик Click для объекта Window или Grid обычным способом было бы невоз- можно. Однако класс UIEIement определяет метод AddHandler (с парным методом RemoveHandler), который получает первый аргумент типа RoutedEvent и позволя- ет установить обработчик для любого маршрутизированного события любого другого элемента того же дерева. Вот как метод AddHandler используется в про- грамме ExamineRoutedEvents для установки обработчика события Click для дру- гих элементов: el.AddHand1 er(Button.Cl 1 ckEvent. new RoutedEventHandlerCAlIPurposeEventHandler)); Установка обработчика для панели (например) чрезвычайно удобна для кон- солидированной обработки событий, поступающих от нескольких дочерних эле- ментов. Если программа запускается на планшетном компьютере, вместо мыши поль- зователь прикасается к кнопке стилусом. Сначала опускающееся событие Preview- StylusDown проходит по дереву от Window к Grid, Button и TextBlock, после чего со- бытие StylusDown поднимается по дереву в обратном направлении. На этой стадии генерируются события PreviewMouseDown и MouseDown, как если бы был использо- ван щелчок левой кнопкой мыши. Когда стилус отрывается от экрана, событие PreviewStylusUp опускается от Windows к Button, а событие StylusUp поднимается от Button к Window. (Объект TextBlock в обработке не участвует.) Затем генерируют- ся события PreviewMouseUp и MouseUp. Стилус всегда генерирует как события мыши, так и события стилуса. Так как щелчок на кнопке был сделан мышью или стилусом, кнопка облада- ет фокусом ввода. Нажмите функциональную клавишу. Событие PreviewKeyDown опускается вниз по дереву от Window к Grid и Button, после чего событие KeyDown поднимается по дереву от Button и Grid к Window. Разумеется, фокусом обладает кнопка, а не TextBlock. При отпускании клавиши событие PreviewKeyUp опускает- ся по дереву, а событие KeyUp поднимается по нему. Теперь нажмите клавишу с буквой. Вы увидите три события PreviewKeyDown и три события KeyDown, за которыми следуют три события PreviewTextlnput и три события Textinput. События Textinput сообщают о вводе текста с клавиатуры. При отпускании клавиши появляются три события PreviewKeyUp с тремя собы- тиями KeyUp. Нажмите пробел. Как и в случае с левой кнопкой мыши, элемент Button про- являет особый интерес к этой клавише. Объект Button прекращает обработку события KeyDown, после чего преобра- зует событие KeyUp в Click. Как вы вскоре увидите, нечто похожее происходит и с клавишей Enter.
176 Глава 9. Маршрутизируемые события ввода RoutedEvent sender Source OriginalSource PreviewKeyDown Window Button Button PreviewKeyDown Grid Button Button PreviewKeyDown Button Button Button PreviewKeyUp Window Button Button PreviewKeyllp Grid Button Button PreviewKeyUp Button Button Button Click Button Button Button Click Grid Button Button Click Window Button Button Определение маршрутизируемого события в классе напоминает определение зависимого свойства. Предположим, для некоторого элемента нужно определить событие, аналогичное Click, но которое бы вы предпочли назвать Knock. Сначала для Knock (а возможно, и для PreviewKnock) определяется статическое поле типа RoutedEvent, доступное только для чтения: public static readonly RoutedEvent KnockEvent; public static readonly RoutedEvent PrevlewKnockEvent; По общепринятой схеме имя поля строится из имени события, за которым следует слово Event. В определении поля или в статическом конструкторе вызывается статический метод EventManager.RegisterRoutedEvent. В первом аргументе передается имя события: KnockEvent = EventManager.Reg1sterRoutedEvent("Knock", Routl ngStrategy.Bubble, typeof(RoutedEventHandler), typeof(YourClass)); PrevlewKnockEvent = EventManager.ReglsterRoutedEvent("PrevlewKnock", Routingstrategy.Tunnel, typeof(RoutedEventHandler). typeof(YourClass)): Обратите внимание: второй аргумент для события Knock равен Routingstrategy. Bubble (поднимающееся событие), а для события PreviewKnock он равен Strategy. Tunnel (опускающееся событие). Последнее значение перечисления RoutingStra- tegy — Direct — означает, что событие не маршрутизируется. Третий аргумент может быть любым делегатом с аргументом типа RoutedEventArgs или его потом- ка. Четвертый аргумент обозначает тип класса, в котором определяется событие. Также необходимо определить события Knock и PreviewKnock. Первая строка программного блока выглядит примерно так же, как при определении обычного события .NET: public event RoutedEventHandler Knock { add { AddHandler(KnockEvent, value): } remove { RemoveHandler(KnockEvent, value); } }
Маршрутизируемые события ввода 177 т п определение события также содержит методы доступа add и remove Хеского маршрутизируемого события. Методы AddHandler и Remove- определяются классом Dependencyobject; аргумент value задает обработ- НикТобытия Определение события PreviewKnock выглядит аналогично, но отно- сится к “бь^ю Previewэти собь1ТИЯ (скорее всего, в методе OnMouseUp) в программе создается объект типа RoutedEventArgs (или одного из его потомков). RoutedEventArgs argsEvent = new RoutedEventArgs(); argsEvent.RoutedEvent = YourClass.PrevlewKnockEvent; argsEvent.Source = this; RalseEvent(argsEvent); argsEvent = new RoutedEventArgs(); argsEvent.RoutedEvent = YourClass.KnockEvent: argsEvent.Source = this: RalseEvent(argsEvent); Конечно, вы узнали свойства RoutedEvent и Source класса RoutedEventArgs. Так- же RoutedEventArgs определяет конструктор, получающий эти два значения. Если значение OriginalSource не задано явно, по умолчанию оно задается равным Source. Метод RaiseEvent определяется UIEIement. Обратите внимание: RaiseEvent сначала вызывается для опускающегося, а затем для поднимающегося события. Полный код программы с событиями Knock и PreviewKnock представлен в гла- ве 10. Мы рассмотрели реализацию событий пользовательского ввода из класса UIEIement, однако эти события также реализованы в ContentElement - классе, от которого наследуют TextElement, FixedDocument и FlowDocument. Объекты Content- Element представляют интерфейсные элементы, которые не могут прорисовать себя на экране, но прорисовываются другими элементами. И UIEIement, и Content- Element реализуют интерфейс IlnputElement, включающий большинство (хотя и не все) событий пользовательского ввода, определяемых классами UIEIement и ContentElement. UIEIement и ContentElement определяют 10 событий, начинающихся с префик- са Mouse, и 8 событий, начинающихся с префикса PreviewMouse. Обработчики событий MouseMove, PreviewMouseMove, MouseEnter и MouseLeave относятся к типу MouseEventHandler, а событие сопровождается объектом типа MouseEventArgs. (Событий PreviewMouseEnter или PreviewMouseLeave не сущест- вует.) Событие MouseMove многократно инициируется во время перемещения указателя мыши по поверхности элемента. События MouseEnter и MouseLeave происходят при входе или выходе указателя мыши из области, занимаемую эле- ментом. Кроме того, UIEIement и ContentElement определяют два вспомогатель- ных свойства, доступных только для чтения. Свойство IsMouseOver истинно, если указатель мыши находится над элементом в любой точке. Свойство IsMouse- DirectlyOver истинно, если указатель мыши находится над элементом, но не над одним из его дочерних элементов.
178 Глава 9. Маршрутизируемые события ввода Вероятно, при обработке событий MouseMove, MouseEnter или MouseLeave по- требуется узнать, какие из кнопок мыши нажаты в данный момент. Объект MouseEventArgs содержит свойства для всех типов кнопок: LeftButton, MiddleButton и RightButton, а также для двух расширенных кнопок XButtonl и XButton2. Каж- дому из этих свойств задается одно из двух значений из перечисления Mouse- ButtonState: Pressed или Released. Возможно, программе также потребуется узнать текущие координаты указа- теля мыши (особенно для событий MouseMove). Метод GetPosition, определяемый классом MouseEventArgs, получает аргумент любого типа, реализующего интер- фейс IlnputElement, и возвращает объект Point в аппаратно-независимых коорди- натах относительно левого верхнего угла элемента. События MouseDown, MouseUp, PreviewMouseDown и PreviewMousellp происходят тогда, когда пользователь нажимает и отпускает кнопку мыши над интерфейсным элементом. Эти события сопровождаются объектами типа MouseButtonEventArgs. Чтобы узнать, какая кнопка инициировала данное событие, проверьте свойст- во ChangedButton. Если пользователь нажал две кнопки одновременно, каждая кнопка генерирует собственное событие MouseDown. Класс MouseButtonEventArgs также определяет свойство с именем Buttonstate, содержащее значение из перечис- ления MouseButtonState. Разумеется, для события MouseDown свойство Buttonstate будет равно MouseButtonState.Pressed, а для события MouseUp — MouseButtonState. Released, но благодаря присутствию этого свойства вы сможете различить два со- бытия, если для MouseDown и MouseUp используется один обработчик. Классы UIEIement и ContentElement также определяют события, относящиеся конкретно к левой или правой кнопке мыши — MouseLeftButtonDown, MouseRight- ButtonDown, MouseLeftButtonUp и MouseRightButtonUp, а также их Preview-версии. Эти события можно считать «вспомогательными» — не существует принципи- альных различий между обработкой событий MouseLeftButtonDown и MouseDown, если логика заключена в следующую команду if: if (args.ChangedButton == MouseButton.Left) { Просто для того, чтобы лучше понять, какое место в этой схеме занимает ле- вая и правая кнопки мыши, вставьте следующий фрагмент в цикл foreach про- граммы ExamineRoutedEvents: el.MouseLeftButtonDown += Al 1PurposeEventHandler; el.MouseLeftButtonUp += AllPurposeEventHandler: el.PreviewMouseLeftButtonDown += AllPurposeEventHandler: el.PreviewMouseLeftButtonUp += AllPurposeEventHandl er; el.MouseRightButtonDown += AllPurposeEventHandler: el.MouseRightButtonUp += AllPurposeEventHandler; el.PreviewMouseRightButtonDown += AllPurposeEventHandler: el.PreviewMouseRightButtonUp += AllPurposeEventHandl er;
|ИяРшрутизируемые события ввода 179 Каждый элемент дерева генерирует событие MouseLeftButtonUp или MouseRight- ButtonUp непосредственно перед событием MouseUp; то же происходит с события- ми ...down и Preview.... События MouseWheel и PreviewMouseWheel передают информацию о количестве поворотов колесика, устанавливаемого на многих современных мышах. Колеси- ко прокручивается не плавно, а с осязаемыми (и часто слышимыми) щелчками. У современных мышей, оснащенных колесиками, прокрутка на одно деление ассоциируется с числовым значением 120. Возможно, в будущем колеса будут вращаться с большей точностью, но в наши дни значение свойства Delta класса MouseWheelEventArgs составляет 120 на одно деление, если колесико поворачивает- ся от пользователя, или -120, если оно поворачивается к пользователю. Наличие колесика проверяется по значению свойства SystemParameters.IsMouseWheelPresent. Программа также может получить информацию о текущей позиции мыши и состоянии кнопок из статических свойств класса Mouse. Класс содержит ста- тические методы для установки и отсоединения обработчиков событий мыши. Класс MouseDevice содержит методы экземпляров для проверки текущей пози- ции мыши и состояния кнопок. Класс Control определяет дополнительное событие мыши с именем Mouse- DoubleClick и метод OnMouseDoubleClick, сопровождаемые вспомогательными объ- ектами типа MouseButtonEventHandler. Текущая позиция мыши представлена на экране растровым изображением, называемым указателем мыши. В WPF указателю мыши соответствует объект типа Cursor. Чтобы связать указатель мыши с интерфейсным элементом, задайте объект Cursor свойству Cursor, определяемому в FrameworkElement. Впрочем, для большей гибкости можно установить обработчик события QueryCursor (или пере- определить метод OnQueryCursor). Это событие инициируется при каждом пере- мещении мыши. Сопровождающий событие объект QueryCursorEventArgs включа- ет свойство с именем Cursor, которому задается соответствующий объект Cursor. Было бы удобно считать, что события MouseDown и MouseUp всегда происходят парами. С точки зрения вашего приложения это определенно не так. Предполо- жим, пользователь перемещает указатель мыши в окно приложения и нажимает кнопку. Окно получает событие MouseDown. Теперь пользователь, продолжая удерживать кнопку, отводит указатель мыши от окна и отпускает кнопку. Окно приложения не будет знать о втором событии. Иногда это нормальное поведение мыши является нежелательным, а про- грамме нужно знать, что происходит с мышью за пределами окна. Представьте графический редактор, в котором пользователь начинает рисовать графический объект, нажимая кнопку мыши. Далее указатель мыши выходит за пределы окна — возможно, совсем ненадолго. Было бы хорошо, если бы приложение про- должало получать события MouseMove и в то время, пока указатель мыши нахо- дится вне окна. Другой пример — стандартный элемент Button. При нажатии левой кнопки мыши генерируется событие Click, но ему предшествует более сложная процеду- ра: если указатель мыши находится над объектом Button, поверхность объекта темнеет (кнопка переходит в «нажатое» состояние). Теперь, не отпуская кнопки
180 Глава 9. Маршрутизируемые события ввода мыши, отведите указатель от объекта Button — поверхность снова приобретает нормальный вид. Если отпустить кнопку мыши и вернуть указатель обратно, объект Button будет знать, что кнопка мыши была отпущена. Такое поведение становится возможным благодаря процессу, называемому захватом мыши. Чтобы захватить мышь, элемент вызывает метод CaptureMouse, определяемый классом UIEIement или ContentElement (или статический метод Capture класса Mouse). Метод возвращает логический признак успеха захвата. Не рекомендуется захватывать мышь где-либо, кроме события MouseDown — это лишь собьет с толку пользователя без сколько-нибудь заметной пользы. После вызова CaptureMouse интерфейсный элемент продолжит получать со- бытия MouseMove и MouseUp даже в том случае, если указатель мыши выйдет за его пределы. При этом свойство IsMouseCaptured, доступное только для чтения, становится истинным, обозначая факт захвата мыши. Когда элемент получает событие MouseUp, он может освободить мышь вызовом ReleaseMouseCapture или статического метода Mouse.Capture с аргументом null. Учтите, что захват мыши может помешать нормальной работе Windows, по- этому система может по своему усмотрению освободить мышь. Например, если мышь не была освобождена при получении события MouseUp, а пользователь щелкнул в другом окне, ваша программа теряет захват мыши. Но даже если про- грамма соблюдает все правила, а системе внезапно потребовалось вывести на эк- ран системно-модальное диалоговое окно, программа тоже потеряет захват мыши. Захват мыши и многие события продемонстрированы в программе Draw- Circles. Поэкспериментируйте с программой, прежде чем рассматривать ее ис- ходный код. Если нажать левую кнопку мыши в окне и перетащить указатель, программа рисует круг в клиентской области окна. Исходная точка щелчка становится центром круга. Нажав правую кнопку мыши над одним из кругов, можно перетащить его в новую позицию. Щелчок средней кнопкой мыши над кругом переключает режим закраски (красный цвет или прозрачный). Клавиша Escape прерывает операции рисования или перетаскивания. DrawCi rcles.cs //------------------------------------------- // DrawCircles.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.DrawCircles { public class DrawCircles : Window { Canvas canv;
Маршрутизируемые события ввода 181 // Поля, относящиеся к рисованию bool isDrawing; Ellipse elips; Point ptCenter: // Поля, относящиеся к перетаскиванию bool isDragging; FrameworkElement el Dragging: Point ptMouseStart, ptElementStart; [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new DrawCirclesQ); } public DrawCircles() { Title = "Draw Circles"; Content = canv = new CanvasO; } protected override void OnMouseLeftButtonDownC MouseButtonEventArgs args) { base.OnMouseLeftButtonDown(args); if (isDragging) return: // Создание нового объекта Ellipse // и его добавление на панель Canvas ptCenter = args.GetPosition(canv): elips = new EllipseO: el ips.Stroke = SystemColors.WindowTextBrush: elips.StrokeThickness = 1: elips.Width = 0: elips.Height = 0: canv.Chi 1dren.Add(eli ps): Canvas.SetLeft(elips, ptCenter.X); Canvas.SetTop(elips. ptCenter.Y); // Захват мыши и подготовка к будущим событиям CaptureMouseO; isDrawing = true: } protected override void OnMouseRightButtonDown( MouseButtonEventArgs args)
182 Глава 9. Маршрутизируемые события ввода base.OnMouseRightButtonDown(args); if (isDrawing) return; // Получение элемента, на котором был сделан щелчок, // и подготовка к будущим событиям ptMouseStart = args.GetPosition(canv); elDragging = canv.InputHitTest(ptMouseStart) as FrameworkElement; if (elDragging != null) { ptElementStart = new Point(Canvas.GetLeft(el Dragging). Canvas.GetTop(elDragging)): isDragging = true; } } protected override void OnMouseDownlMouseButtonEventArgs args) { base.OnMouseDown(args); if (args.ChangedButton == MouseButton.Middle) Shape shape = canv.InputHitTest(args.GetPosition(canv)) as Shape; if (shape != null) shape.Fill = (shape.Fill == Brushes.Red ? Brushes.Transparent : Brushes.Red); } } protected override void OnMouseMovelMouseEventArgs args) { base.OnMouseMove(args); Point ptMouse = args.GetPosition(canv); // Перемещение и изменение размеров эллипса if (isDrawing) { double dRadius = Math.Sqrt( Math.Pow(ptCenter.X - ptMouse.X, 2) + Math.Pow(ptCenter.Y - ptMouse.Y, 2)); Canvas.SetLeft(elips, ptCenter.X - dRadius); Canvas.SetTop(elips, ptCenter.Y - dRadius); elips.Width =2* dRadius; el ips. Height = 2* dRadius; } // Перемещение эллипса else if (isDragging)
маршрутизируемые события ввода 183 Canvas.SetLeft(elDraggi ng, ptElementStart.X + ptMouse.X - ptMouseStart.X); Canvas.SetTop(elDraggi ng, ptElementStart.Y + ptMouse.Y - ptMouseStart.Y): } protected override void OnMousellp(MouseButtonEventArgs args) { base.OnMousellp(args); // Завершение операции рисования if (isDrawing && args.ChangedButton == MouseButton.Left) elips.Stroke = Brushes.Blue; elips.StrokeThickness = Math.Min(24, elips.Width / 2); el ips.Fill = Brushes.Red: isDrawing = false; ReleaseMouseCaptureO; // Выход из режима захвата else if (isDragging && args.ChangedButton == MouseButton.Right) isDragging = false; } } protected override void OnTextlnputdextCompositionEventArgs args) { base.OnTextlnput(args); // Нажатие Escape прерывает рисование и перетаскивание if (args.Text.IndexOf('\xlB') != -1) if (isDrawing) ReleaseMouseCapture(); else if (isDragging) { Canvas.SetLeft(elDragging, ptElementStart.X); Canvas.SetTop(elDragging. ptE1ementStart.Y); isDragging = false; } } } protected override void OnLostMouseCapture(MouseEventArgs args) { base.OnLostMouseCapture(args); // Аномальное завершение рисования: удаление эллипса if (isDrawing)
184 Глава 9. Маршрутизируемые события ввода { canv.Chi 1dren.Remove(el ips); isDrawing = false: } } } } Клиентская область окна накрывается панелью Canvas. При рисовании мышью почти всегда используется объект Canvas. Вместо установки обработчиков собы- тий класс окна переопределяет семь методов, относящихся к работе с мышью. Целью программы DrawCircles является не столько логическая целостность, сколь- ко демонстрация различных способов работы с мышью. Например, программа содержит раздельные методы OnLeftMouseButtonDown и OnRightMouseButtonDown для инициирования операций рисования и перетаскивания, но завершение этих операций объединяется в одном методе OnMouseUp. Окно захватывает мышь для рисования, но не для перетаскивания. Внутри метода OnLeftMouseButtonDown программа создает объект Ellipse, добав- ляет его в коллекцию дочерних объектов Canvas и захватывает мышь. Метод также задает полю isDrawing значение true, чтобы обработчики событий знали, что сейчас происходит в программе. Дальнейшая обработка выполняется в переопределении OnMouseMove. Я хотел, чтобы исходный щелчок обозначал центр круга, а периметр отслеживал переме- щения указателя мыши. Для этого потребовалось организовать пересчет свойств Width и Height, а также присоединенных свойств Canvas.LeftProperty и Canvas.Right- Property. В альтернативном способе (более подходящем для прямоугольника) позиция исходного щелчка находится в одном углу, а текущая позиция указа- теля мыши — в другом углу по диагонали от него. Это не обязательно левый верхний и правый нижний углы, потому что пользователь может сместить мышь налево или вверх от исходной позиции. В переопределении OnMouseUp первая секция относится к операции рисования. Метод задает для эллипса синий периметр и красную заливку, задает isDrawing истинное значение и освобождает мышь. Освобождение мыши инициирует вы- зов OnLostMouseCapture, но поскольку флаг isDrawing уже равен false, дальнейшая работа не выполняется. Обратите внимание на переопределение OnTextlnput: если пользователь нажмет клавишу Escape, а свойство isDrawing истинно, метод вызы- вает ReleaseMouseCapture. Теперь результирующему вызову OnLostMouseCapture ос- тается удалить объект Ellipse из коллекции дочерних объектов Canvas. Правая кнопка мыши инициирует операцию перетаскивания. Метод OnRight- MouseButtonDown вызывает метод InputHitTest объекта Canvas, определяя, находит- ся ли какой-либо интерфейсный элемент под указателем мыши. Метод сохраняет позицию мыши и позицию элемента в полях. При последующих вызовах OnMouse- Move программа перемещает эллипс. Так как мышь не была захвачена во время операции, при выходе указателя мыши за пределы окна эллипс перестает дви- гаться. Если теперь отпустить кнопку мыши и вернуть указатель в окно, эллипс
185 Маршрутизируемы е события ввода продолжит двигаться за мышью, хотя кнопка не нажата. Впрочем, проблема не столь серьезная — достаточно щелкнуть правой кнопкой мыши, и операция пе- ретаскивания завершится. В реальном приложении следовало бы захватить мышь на время перетаскивания. Наконец, средняя кнопка переключает кисть заполнения внутренней области между Brushes. Red и Brushes.Transparent. В первоначальном варианте программы вместо Brushes.Transparent свойство Fill объекта Ellipse задавалось равным null. Однако в этом случае эллипс реагировал лишь в том случае, если щелчок был сделан на периметре. Пользователь уже не мог щелкнуть внутри эллипса, чтобы переместить его или хотя бы переключить кисть Fill. Программа хороню демонстрирует, почему разработчики Windows Presentation Foundation должны были реализовать маршрутизируемую обработку событий. При запуске программы события инициируются панелью Canvas, а не объектом Window. Установить обработчики событий для Canvas несложно, но что про- изойдет после того, как пользователь нарисует в окне несколько кругов? Если щелкнуть на существующем круге, источником события мыши становится объ- ект Ellipse. В системе без маршрутизации событий нам пришлось принудительно передавать события мыши панели Canvas — вероятно, посредством снятия флага IsEnabled объекта Ellipse. Маршрутизация событий обеспечивает значительно большую гибкость в выборе структуры программы. На планшетном компьютере ввод со стилуса преобразуется в события мыши, поэтому для управления программой DrawCircles можно использовать стилус. Впрочем, WPF позволяет разрабатывать программы специально для планшет- ных компьютеров. Существуют три общих программных интерфейса, которые можно использовать для этой цели. Среди программных интерфейсов для планшетных ПК в WPF на самом вы- соком уровне находится интерфейс InkCanvas — на первый взгляд название на- поминает имя очередной панели, но это не так. Класс InkCanvas является произ- водным от FrameworkElement. Раньше я уже упоминал, что ввод со стилуса всегда преобразуется в ввод с мыши. Класс InkCanvas осуществляет обратное преобра- зование: он реагирует на ввод с мыши и интерпретирует его как ввод со стилуса. Когда вы рисуете на объекте InkCanvas мышью или стилусом, объект воспроиз- водит линии па поверхности и сохраняет их в своем свойстве Strokes, содержа- щем коллекцию штриховых объектов Stroke — фактически речь идет о ломаных линиях с присоеденнными графическими атрибутами. Каждый штрих определя- ется прикосновением стилуса к экрану, его перемещением и отрывом от экрана. Пи работе с мышью штрих начинается там, где была нажата левая кнопка мыши, и завершается там, где пользователь отпустил кнопку. Пример использования InkCanvas приведен в главе 22. Низкоуровневый программный интерфейс WPF для работы с планшетными ПК находится в пространстве имен Windows.Input.StylusPlugins. Модули стилуса представляют собой классы, написанные разработчиком для воспроизведения, модификации или сохранения ввода со стилуса.
186 Глава 9. Маршрутизируемые события ввода Промежуточное положение занимает коллекция событий, определяемых в UI- EIement и ContentElement, имена которых начинаются с префиксов Stylus и Preview- Stylus. Они очень похожи на события мыши, хотя операции, выполняемые с уст- ройством, могут выглядеть иначе. Например, событие MouseDown происходит при нажатии кнопки мыши пользователем; событие StylusDown происходит при прикос- новении стилусом к экрану. А событие StylusButtonDown обозначает не прикосно- вение к экрану, а нажатие кнопки на боковой поверхности стилуса, изменяющей интерпретацию события StylusDown. (Например, прикосновение к экрану с нажа- той кнопкой может вызывать контекстное меню.) Следующая программа работает только на планшетных компьютерах. При получении события StylusDown она создает два объекта типа Polyline и включает их в коллекцию дочерних объектов Canvas. (Класс Polyline, как и Ellipse, наследует от класса Shape.) Программа захватывает стилус и добавляет дополнительные точки в каждую ломаную при получении событий StylusMove. Построение лома- ных завершается событием StylusUp или нажатием клавиши Escape. Программа рисует вторую ломаную со смещением от первой; так в реальном времени имитируется эффект отбрасываемой тени. Обратите внимание: в кол- лекцию Points ломаной переднего плана всегда заносятся координаты ptStylus, а в коллекцию ломаной тени заносится сумма координат ptStylus и vectShadow. ShadowTheStylus.cs //------------------------------------------------- // ShadowTheStylus.cs (с) 2006 by Charles Petzold //------------------------------------------------- using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media; using System.Windows.Shapes: namespace Petzold.ShadowTheSty1 us { public class ShadowTheSty1 us : Window { // Определения констант static readonly SolidColorBrush brushstylus = Brushes.Blue: static readonly SolidColorBrush brushshadow = Brushes.LightBlue: static readonly double widthstroke = 96 / 2.54: // 1 cm static readonly Vector vectShadow = new Vector(widthstroke / 4. widthstroke / 4): // Дополнительные поля для операций перемещения стилуса Canvas canv; Polyline polyStylus. polyShadow; bool isDrawing;
маршрутизируемые события ввода 187 [STAThread] public static void MainO Application app = new ApplicationO; app.Run(new ShadowTheStylusO); public ShadowTheStylusO { Title = "Shadow the Stylus"; // Создание панели Canvas для содержимого окна canv = new Canvas(); Content = canv; } protected override void OnStylusDown(StylusDownEventArgs args) { base.OnStylusDown(args); Point ptStylus = args.GetPosition(canv); // Создание основного объекта Polyline // с закругленными концами отрезков polyStylus = new PolylineO; polyStylus.Stroke = brushstylus; polyStylus.StrokeThickness = widthstroke; polyStylus.StrokeStartLineCap = PenLineCap.Round; polyStylus.StrokeEndLineCap = PenLineCap.Round; polyStylus.StrokeLineJoin = PenLineJoin.Round; polyStylus.Points = new PointCollectionO; polySty1 us.Poi nts.Add(ptSty1 us); // Создание объекта Polyline для имитации тени polyShadow = new PolylineO; polyShadow.Stroke = brushShadow; polyShadow.StrokeThickness = widthstroke; polyShadow.StrokeStartLineCap = PenLineCap.Round; polyShadow.StrokeEndLineCap = PenLineCap.Round; polyShadow.StrokeLineJoin = PenLineJoin.Round; polyShadow.Points = new PointCollectionO; polyShadow.Points.Add(ptStylus + vectShadow); // Тень вставляется перед ломаными переднего плана canv.Children.Insert(canv.Children.Count / 2, polyShadow); // Основная ломаная добавляется последней canv.Chi 1dren.Add(polySty1 us); Capturestylus О; isDrawing = true: args.Handled = true;
188 Глава 9. Маршрутизируемые события ввода protected override void OnStylusMove(StylusEventArgs args) { base.OnSty1usMove(args): if (IsDrawing) Point ptStylus = args.GetPosition(canv): polyStylus.Poi nts.Add(ptStylus); polyShadow.Points.Add(ptStylus + vectShadow): args.Handled = true; } } protected override void OnStyl usllp(Styl usEventArgs args) { base.OnStylusllp(args); if (isDrawing) { isDrawing = false: ReleaseStylusCapture(): args.Handled = true; } } protected override void OnTextInput(TextCompositionEventArgs args) { base.OnTextlnput(args); // Рисование завершается клавишей Escape if (isDrawing && args.Text.IndexOf(’\xlB*) != -1) ReleaseStylusCapture(); args.Handled = true: } } protected override void OnLostStylusCapture(StylusEventArgs args) { base.OnLostStylusCapture(args): // Аномальное завершение рисования: удаление ломаных if (isDrawing) { canv.Chi 1dren.Remove(polyStylus): canv.Chi 1dren.Remove(polyShadow); isDrawing = false; } } } Одна из важнейших команд этой программы расположена в конце метода OnStylusDown, при добавлении ломаной, имитирующей тень, в середину коллек- ции Children объекта Canvas: canv.Children.Insert(canv.Children.Count / 2, polyShadow);
маршрутизируемые события ввода 189 Основная ломаная просто включается в конец коллекции: canv.Chi 1dren.Add(po1уSty1 us): Стоит напомнить, что объекты коллекции Children воспроизводятся по поряд- ку, поэтому программа сначала рисует все тени, а за ними — все линии передне- го плана. Если ваша программа должна обрабатывать как события мыши, так и собы- тия стилуса, вероятно, она должна отличать события, сгенерированные опера- циями с мышью, от событий, сгенерированных стилусом. Класс MouseEventArgs содержит свойство StylusDevice. Для событий, сгенерированных мышью, свойст- во равно null, а для событий, сгенерированных стилусом, оно равно StylusDevice. Аналогичное свойство присутствует и в классе StylusEventArgs, поэтому вы даже сможете идентифицировать конкретный стилус, если компьютер оснащен не- сколькими устройствами такого рода. Вероятно, большая часть обработки клавиатуры в приложениях WPF будет выполняться элементами TextBox и RichTextBox или элементами Scrollviewer, реа- гирующими на некоторые клавиши управления курсором. Тем не менее в неко- торых ситуациях вам придется обрабатывать ввод с клавиатуры самостоятельно. Интерфейсный элемент или элемент управления является источником со- бытий клавиатурного ввода тогда и только тогда, когда он обладает фокусом ввода — иначе говоря, если свойство IsKeyboardFocused истинно. События кла- виатуры являются маршрутизируемыми, поэтому все предки в дереве элемента, обладающего фокусом, могут участвовать в обработке ввода. Для таких элемен- тов свойство IsKeyboardFocusedWithin равно true. Чтобы элемент мог получить фокус ввода, его свойство Focusable должно быть равно true. (В отличие от других клавиатурных свойств, методов и событий, свойство Focusable определяется классом FrameworkElement.) Передача фокуса ввода по дереву элементов обычно осуществляется щелчком на элементах, на- жатием клавиши Tab или клавиш со стрелками. Программа может передать фокус конкретному элементу, вызвав метод Focus для этого элемента. Для расширенной передачи фокуса используется метод Move- Focus, определяемый классом FrameworkElement. При получении и потере фокуса элементы генерируют события GotKeyboard- Focus и LostKeyboardFocus. Свойство Source аргументов события всегда обозначает элемент, получающий или утрачивающий фокус ввода. Класс KeyboardFocused- ChangedEventArgs также определяет два события типа IlnputElement с именами OldFocus и NewFocus. Ввод с клавиатуры связан с тремя типами событий, которые обычно проис- ходят в следующем порядке: О KeyDown — аргументы типа KeyEventArgs; О Textinput — аргументы типа TextCompositionEventArgs; О KeyUp — аргументы типа KeyEventArgs. При нажатии клавиш Shift, функциональных клавиш или клавиш со стрелка- ми событие Textinput не генерируется. Если ввести прописную букву А (нажать
190 Глава 9. Маршрутизируемые события ввода клавишу Shift вместе с клавишей А), вы получите два события KeyDown, событие Textinput и еще два события Keyllp в момент отпускания клавиш. Клавиша, задействованная в событии, определяется свойством Key объекта KeyEventArgs. Ее значения объединены в громадное перечисление Key, содержа- щее более 200 элементов. Следующая программа поможет вам лучше понять события клавиатуры. Она выводит свойства объектов аргументов, сопровождающих события KeyDown, Text- Input и KeyUp. ExamineKeystrokes.es // // ExamineKeystrokes.es (с) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ExamineKeystrokes { class ExamineKeystrokes : Window { StackPanel stack; Scroll Vi ewer scroll; string strHeader = "Event Key Sys-Key Text " + "Ctrl-Text Sys-Text Ime KeyStates " + "IsDown I slip IsToggled IsRepeat "; string strFormatKey = "{0.-10}{l.-20}{2.-10} " + " {3,-10}{4.-15}{5,-8}{6.-7}{7.-10}{8.-10}"; string strFormatText = "{0,-10} " + "{1,-10}{2,-10}{3,-10}"; [STAThread] public static void MainO { Application app = new Application(); app.Run(new ExamineKeystrokes()); } public ExamineKeystrokesO { Title = "Examine Keystrokes"; FontFamily = new FontFamily("Courier New"); Grid grid = new GridO: Content = grid;
кт.нрутизируемые события ввода 191 // Одна строка в режиме Auto, // другая заполняет все оставшееся место RowDefinition rowdef = new RowDefinitionО; rowdef.Height = GridLength.Auto; gri d.RowDefi ni ti ons.Add(rowdef); grid.RowDefinitions.Add(new RowDefini ti on()); // Вывод заголовка TextBlock textHeader = new TextBlockO; textHeader.FontWeight = Fontweights.Bold; textHeader.Text = strHeader; gri d.Chi 1dren.Add(textHeader); // Создание StackPanel как дочернего объекта Scrollviewer // для отображения событий scroll = new ScrolIViewerO; grid.Chi 1dren.Add(scrol1); Grid.SetRow(scroll, 1); stack = new StackPanel0; scroll.Content = stack; } protected override void OnKeyDown(KeyEventArgs args) { base.OnKeyDown(args); DisplayKeylnfo(args); } protected override void OnKeyUp(KeyEventArgs args) { base.OnKeyUp(args); DisplayKeylnfo(args); protected override void OnTextInput(TextCompositionEventArgs args) { base.OnTextlnput(args); string str = String.Format(strFormatText. args.RoutedEvent.Name, args.Text,args.Control Text, args.SystemText); Displaylnfo(str); void DisplayKeyInfo(KeyEventArgs args) { string str = String.Format(strFormatKey, args.RoutedEvent.Name, args.Key. args.SystemKey. args.ImeProcessedKey, args.KeyStates, args.IsDown. args.lsllp, args. IsToggled, args. IsRepeat): Displaylnfo(str);
192 Глава 9. Маршрутизируемые события ввода void DisplayInfo(string str) TextBlock text = new TextBlockО; text.Text = str; stack.Children.Add(text); scrol1.Scrol1ToBottom(); По событиям также можно получить информацию о состоянии клавиату- ры. Класс Keyboard содержит статическое свойство с именем PrimaryDevice типа KeyboardDevice, а класс KeyboardDevice содержит методы для проверки состояния каждой клавиши на клавиатуре. Некоторые программы следующих двух глав демонстрируют обработку кла- виатуры и мыши в практическом контексте. Класс MedievalButton из главы 10 и RoundedButton из главы И имитируют обработку ввода с мыши и клавиату- ры, реализованную в обычных кнопках. Общие принципы работы с клавиатурой и мышью продемонстрированы в программах CalculatelnHex и ColorGrid главы 11.
W Пользовательские элементы В этой главе (и двух следующих) представлены способы создания пользова- тельских интерфейсных элементов и элементов управления, лучше всего при- способленные для процедурных языков типа С#. Во второй части книги мы рас- смотрим другие способы создания пользовательских элементов на базе XAML, а также стили и шаблоны, которые помогают в настройке элементов. Классы пользовательских элементов почти всегда являются производными от FrameworkElement — по аналогии с классами Image, Panel, TextBlock и Shape. Как правило, пользовательские элементы наследуют от Control или (если повезет) от одного из классов, производных от Control, — скажем, Contentcontrol. При проектировании нового элемента возникает постоянный вопрос: объяв- лять ли его производным от FrameworkElement или от Control? Объектно-ориенти- рованный подход рекомендует наследовать от самого низшего класса иерархии, предоставляющего необходимые возможности. Для некоторых классов им одно- значно будет класс FrameworkElement. Тем не менее класс Control добавляет в Frame- workElement ряд полезных свойств: Background, Foreground и шрифтовые свойства. Несомненно, при выводе текста эти свойства вам пригодятся. Однако класс Control добавляет и другие свойства, которые вам, возможно, хотелось бы игнорировать, но все равно придется реализовать: HorizontalContentAlignment, VerticalContentAlignment, BorderBrush, BorderThickness и Padding. В наибольшей степени различия между интерфейсными элементами и элемен- тами управления проявляются в свойстве Focusable. Хотя класс FrameworkElement определяет это свойство, по умолчанию оно равно false. Класс Control переопре- деляет значение по умолчанию равным true; это наводит на мысль, что элемент управления представляет собой интерфейсный элемент, способный получать фокус ввода с клавиатуры. Конечно, ничто не помешает наследовать от Frame- workElement и задать Focusable равным true или наследовать от Control и задать Focusable равным false (как это делают некоторые элементы), однако это инте- ресный и удобный критерий, по которому можно отличать интерфейсные эле- менты от элементов управления.. В конце главы 3 была представлена программа RenderTheGraphic с классом, производным от FrameworkElement. Вот как выглядел этот класс. SimpleEllipse.cs // // SimpleEllipse.cs (с) 2006 by Charles Petzold
194__________________________________ Глава 10. Пользовательские элементы //------------------------------------------------- using System; using System.Windows; using System.Windows.Media; namespace Petzold.RenderTheGraphic { class SimpleEllipse : FrameworkElement { protected override void OnRender(Drawingcontext de) { dc.DrawEllipse(Brushes.Blue. new Pen(Brushes.Red, 24). new Point(RenderSize.Width / 2. RenderSize.Height /2). RenderSize.Width / 2. RenderSize.Height /2): } } } Виртуальный метод OnRender определяется классом UIEIement. Его единствен- ным аргументом является объект типа DrawingContext. (Опытный Windows-про- граммист вряд ли устоит перед искушением назвать его сокращением DC в честь контекстов устройств, использовавшихся в приложениях Win 16 и Win32; два вида контекстов служат сходным целям, хотя и не являются функционально эквивалентными.) В классе DrawingContext определяется набор графических ме- тодов, представляющих самый нижний уровень графических операций, на ко- тором программа еще может называться «чистым WPF-приложением». Пара таких методов будет описана в следующих двух главах, а другие методы встре- тятся нам позже. Код метода OnRender должен считать, что ширина и высота поверхности ри- сования с началом координат (0, 0) задаются свойством RenderSize (также опре- деляемом в UIEIement). В методе OnRender класса SimpleEllipse первый аргумент DrawEllipse содержит синюю кисть для заполнения внутренней области эллипса, а второй — красное перо для закраски контура. Третий аргумент содержит объ- ект Point, представляющий центр эллипса, а последние два аргумента — размеры полуосей. Конечно, изображение, нарисованное в методе OnRender, не воспроизводится прямо на экране. Графический объект, определенный в OnRender, сохраняется графической системой WPF и используется в сочетании с другими визуальны- ми объектами для формирования изображения. Графический объект хранится до тех пор, пока он не будет заменен последующим вызовом OnRender. Вызо- вы OnRender могут происходить в любой момент, когда системе потребуется обновить визуальное представление элемента. Благодаря сохранению графики обычно это происходит гораздо реже, чем выдача сообщений WM_PAINT в Win32 или сообщений Paint в программах Windows Forms. Например, вызов OnRender не обязателен, когда визуальный объект открывается при перемещении дру- гого окна. С другой стороны, при изменении размеров элемента вызывается метод OnRender. Вызов OnRender также может быть обусловлен вызовом метода InvalidateVisual (определяемого классом UIEIement).
П^йовательао^еэлег™ 195 Метод OnRender обычно использует свойство RenderSize для рисования или по крайней мере для определения границ вывода. Откуда берется это свойство? Как будет показано в этой и следующих главах, RenderSize вычисляется классом UIEIement по нескольким факторам. Размеры RenderSize дублируются в защищен- ных свойствах Actualwidth и ActualHeight элемента. После всех экспериментов с размерами элементов в предыдущих главах ста- новится очевидно, что значение RenderSize тесно связано с размером контейнера в котором отображается элемент (в программе RenderTheGraphic контейнером является клиентская область окна). Однако на RenderSize могут влиять и другие факторы. Так, если задать для эллипса свойство Margin, RenderSize уменьшится на величину отступов. Если в программе явно задаются свойства Width или Height элемента, они за- меняют размеры контейнера при вычислении RenderSize. Если ширина контей- нера будет меньше Width, часто изображение усекается. Размеры элементов также зависят от свойств HorizontalAlignment и Vertical- Alignment. Попробуйте включить следующий фрагмент в программу RenderThe- Graphic: el ips.HorizontalAlignment = HorizontalAlignment.Center: elips.VerticalAlignment = VerticalAlignment.Center: Эллипс сворачивается в крошечный шарик. Это произошло из-за того, что у RenderSize значения Width и Height оказались равны нулю и видимой остается лишь часть контура эллипса. Класс пользовательского элемента не должен использовать открытые свойства для определения своих желательных размеров. Класс пользовательского элемента не должен задавать свои свойства Width, Height, Min Width, MaxWidth, MinHeight или MaxHeight. Эти свойства предназначены для потребителей класса (то есть клас- сов, создающих экземпляры класса пользовательского элемента). Вместо этого класс пользовательского элемента объявляет свой желательный размер, переопределяя метод MeasureOverride из класса FrameworkElement. Имя метода MeasureOverride выглядит несколько странно, но это объяснимо: он похож на метод UIEIement с именем MeasureCore. Класс FrameworkElement переопределяет MeasureCore как фиксированный (sealed) метод; это означает, что метод не может переопределяться в классах, производных от FrameworkElement. Класс Frame- workElement определяет MeasureOverride как замену для MeasureCore. Framework- Element требует иного подхода к определению размеров элементов, чем UIEIement, из-за включения свойства Margin. Интерфейсный элемент требует выделения места для своих внешних отступов, но сам это место не использует. Класс, производный от FrameworkElement, переопределяет MeasureOverride при- мерно так: protected override Size MeasureOverride(Size sizeAvailable) { Size sizeDesired: return sizeDesired:
196 Глава 10. Пользовательские элементы Вызов MeasureOverride всегда предшествует первому вызову OnRender. В буду- щем могут последовать дополнительные вызовы OnRender, если изображение эле- мента необходимо обновить, но не произошло ничего, что бы влияло на размеры элемента. Программа может обеспечить принудительный вызов MeasureOverride, вызывая для элемента метод InvalidateMeasure. Аргумент sizeAvailable обозначает размер, доступный для элемента. Свойства Width и Height этого объекта Size могут находиться в интервале от 0 до положи- тельной бесконечности. Например, в окне, которое просто задает элемент своему свойству Content, sizeAvailable соответствует размеру клиентской области. Но если свойство SizeTo- Content окна будет задано равным SizeToContent.WidthAndHeight, то свойства Width и Height становятся равными Double.Positiveinfinity — предполагается, что окно мо- жет расти до любых размеров, необходимых элементу. Если свойство SizeToCon- tent задано равным SizeToContent.Width, то значение sizeAvailable.Width будет беско- нечным, a sizeAvailable.Height будет соответствовать высоте клиентской области. (Впрочем, если пользователь затем изменит размер окна перетаскиванием рам- ки, sizeAvailable возвращается к фактическому размеру клиентской области.) Для примера возьмем дочерний элемент панели StackPanel со стандартной вер- тикальной ориентацией. В этом случае sizeAvailable.Width соответствует ширине StackPanel (которая, конечно, обычно совпадает с шириной контейнера), а значе- ние sizeAvailable.Height будет бесконечным. Допустим, элемент является дочерним элементом Grid и находится в столбце с шириной GridLength=300. Тогда значение sizeAvailable.Width тоже равно 300. Если бы для ширины столбца был задан режим GridLength.Auto, то значение sizeAvailable.Width будет бесконечным, потому что столбец будет увеличиваться в соответствии с размерами элемента. Наконец, в режиме GridUnitType.Star значе- ние sizeAvailable.Width будет обозначать некоторую часть свободного пространст- ва, доступного для данного столбца. Если для элемента задано свойство Margin, часть пространства тратится на внешние отступы и для самого элемента остается меньше места. Свойство size- Available.Width уменьшается на сумму свойств Left и Right объекта Margin, a sizeAvai- lable.Height — на сумму свойств Тор и Bottom. Конечно, бесконечные размеры ос- таются бесконечными независимо от размера внешних отступов. Все сказанное несколько изменяется при установке каких-либо свойств эле- мента из следующего списка: Width, MinWidth, MaxWidth, Height, MinHeight, MaxHeight. Эти свойства устанавливаются для элемента его потребителем. По умолчанию все они равны NaN (Not a Number, «не число»). Только если программа задаст значения этих свойств, они начнут влиять на размеры и расположение элементов. Если свойство Width задано, то поле sizeAvailable.Width будет равно Width (ана- логично для Height). С другой стороны, если задано свойство MinWidth, то значе- ние sizeAvailable.Width будет больше или равно MinWidth (а это означает, что оно по-прежнему может быть бесконечным). Если задано свойство MaxWidth, то зна- чение sizeAvailable.Width будет меньше либо равно MaxWidth.
Пользовательские элементы 197 Для обозначения «естественного» размера элемента класс должен использо- вать свое переопределение метода MeasureOverride. Естественные размеры наибо- лее очевидны для TextBlock и объекта Image, отображающего растровое изобра- жение с метрическими размерами. Если естественного размера не существует, элемент должен вернуть минимальный приемлемый размер (ноль или какая-ни- будь малая величина). Если метод MeasureOverride не переопределялся, базовая реализация класса FrameworkElement возвращает ноль. Это вовсе не означает, что элемент будет иметь нулевые размеры! Как вы помните, программа RenderTheEllipse рисовала нормальный эллипс, хотя класс SimpleEllipse не переопределяет MeasureOverride и позволяет базовой реализации вернуть ноль. Построение элементов с нулевым размером чаще всего предотвращают значения Stretch свойств HorizontalAlignment и VerticalAlignment, используемые по умолчанию. Если вы определяете простой элемент без дочерних элементов (как будет показано в следующей главе, в этом случае все меняется), вполне возможно, что вашему методу MeasureOverride даже не придется анализировать аргумент sizeAvailable. Исключение составляют ситуации, когда элемент должен сохранять фиксированные пропорции. Например, класс Image возвращает из Measure- Override значение, основанное на размере выводимого изображения и значении свойства Stretch. Если свойство равно Stretch.None, то MeasureOverride возвращает метрический размер изображения. Для значения Stretch.Uniform (используется по умолчанию) MeasureOverride использует аргумент sizeAvailable для вычисления размера, сохраняющего правильные пропорции, у которого ширина или высота совпадает со свойством Width или Height объекта sizeAvailable. Если один из раз- меров sizeAvailable бесконечен, то другой размер используется для вычисления отображаемого размера элемента. Если бесконечны оба размера, MeasureOverride возвращает метрический размер изображения. Для значения Stretch.Fill Measure- Override просто возвращает аргумент sizeAvailable (если хотя бы одно из свойств бесконечно, MeasureOverride возвращется к логике Stretch.Uniform). Впрочем, класс Image скорее является исключением. Большинство элементов, не имеющих естественного размера, должно возвращать ноль или малую вели- чину. Метод MeasureOverride не должен возвращать объект Size с бесконечным размером, даже если один или оба размера в аргументе MeasureOverride беско- нечны. Элемент не должен пытаться учитывать свои свойства Width, Height и т. д. во время выполнения метода MeasureOverride. Эти свойства уже были приняты во внимание при вызове MeasureOverride. Как упоминалось ранее, элемент не дол- жен задавать свои свойства Width и Height; эти свойства предназначены для по- требителей. Стандартный класс Ellipse из библиотеки Shapes (а вернее, класс Shape, произ- водным от которого является Ellipse) возвращает объект Size с размерами, равны- ми его свойству Thickness, — обычно это небольшое число. Поведение следующе- го класса в гораздо большей степени напоминает поведение Ellipse.
198 Глава 10. Пользовательские элементы BetterEllipse.cs //----------------------------------------------- // BetterEllipse.cs (с) 2006 by Charles Petzold //----------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.RenderTheBetterEl1ipse { public class BetterEllipse : FrameworkElement { // Зависимые свойства public static readonly Dependencyproperty Fill Property; public static readonly Dependencyproperty StrokeProperty; // Открытые интерфейсы к зависимым свойствам public Brush Fill { set { SetValue(FillProperty, value); } get { return (Brush)GetValue(FillProperty); } } public Pen Stroke { set { SetValue(StrokeProperty, value); } get { return (Pen)GetValue(StrokeProperty); } } // Статический конструктор static BetterEllipse() { Fi11 Property = Dependencyproperty.Regi ster(”Fi11". typeof(Brush). typeof(BetterEllipse). new FrameworkPropertyMetadata(null. FrameworkPropertyMetadataOptions.AffectsRender)); Strokeproperty = Dependencyproperty.Register("Stroke", typeof(Pen), typeof(BetterEllipse), new FrameworkPropertyMetadata(null. FrameworkPropertyMetadataOptions.AffectsMeasure)); } // Переопределение MeasureOverride protected override Size MeasureOverride(Size sizeAvailable) {
пользовательские элементы 199 Size sizeDesired = base.MeasureOverrlde(sizeAvai1ablе); If (Stroke != null) sizeDesired = new Size(Stroke.Thickness, Stroke.Thickness): return sizeDesired: } // Переопределение OnRender protected override void OnRender(DrawingContext de) { Size size = RenderSize; // Регулировка размера воспроизведения с учетом толщины Реп if (Stroke != null) { size.Width = Math.Max(0, size.Width - Stroke.Thickness): size.Height = Math.Max(0, size.Height - Stroke.Thickness): } // Рисование эллипса dc.DrawEllipse(Fi11, Stroke, new Point(RenderSize.Width / 2, RenderSize.Height /2). size.Width / 2, size.Height / 2); } } Присмотревшись к обычному классу Shape, вы увидите, что в нем определя- ется группа свойств, начинающихся с префикса Stroke. Эти свойства управляют внешним видом линий — в частности, контура Ellipse. Вместо того чтобы реали- зовывать все эти свойства Stroke в своем классе, я решил определить всего одно свойство Stroke класса Реп; в сущности, этот класс инкапсулирует все свойства, явно определяемые в Shape. Обратите внимание: MeasureOverride возвращает раз- мер по толщине объекта Реп (если последний существует), a OnRender уменьша- ет размер полуосей эллипса па величину свойства Thickness. Я решил, что свойства Fill и Stroke должны поддерживаться зависимыми свой- ствами FillProperty и StrokeProperty, чтобы класс был готов к анимации. Обратите внимание на определение FrameworkPropertyMetadata в статическом конструкторе: свойство Fill содержит флаг AffectsRender, а свойство Stroke — флаг AffectsMeasure. Изменение свойства Fill приводит к вызову метода InvalidateVisual, в результате чего генерируется новый вызов OnRender. С другой стороны, изменение свойства Stroke приводит к вызову InvalidateMeasure, генерирующему вызов MeasureOverride с последующим вызовом OnRender. Отличие в том, что размер элемента, опреде- ляемый методом MeasureOverride, зависит от Реп, но не от Brush. Как нетрудно убедиться при запуске программы RenderTheBetterEllipse, те- перь контур эллипса точно укладывается в клиентскую область окна.
200 Глава 10. Пользовательские элементы RenderTheBetterEllipse.cs // // RenderTheBetterEllipse.cs (с) 2006 by Charles Petzold //-------------------------------------------------------- using System; using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: namespace Petzold.RenderTheBetterEl1ipse { public class RenderTheBetterEllipse : Window { [STAThread] public static void MainO { Application app = new Application): app.Run(new RenderTheBetterElTi pse()): } public RenderTheBetterEllipseO { Title = "Render the Better Ellipse": BetterEllipse elips = new BetterEllipseO: elips.Fill = Brushes.AliceBlue: el ips.Stroke = new Pen( new LinearGradientBrush(Colors.CadetBlue, Colors.Chocolate, new Pointd, 0), new Point(0. 1)), 24): // 1/4 дюйма Content = elips: } } } Предположим, вы хотите включить в BetterEllipse код для вывода текста по центру элемента. Поскольку методы MeasureOverride и OnRender уже реализова- ны, напрашивается простое решение — включить в OnRender код для вызова ме- тода DrawText класса DrawingContext. Метод DrawText сам по себе выглядит доста- точно просто: de.DrawText(formtxt, pt): Второй аргумент определяет начальную точку для вывода текста (по умолча- нию базовая точка для вывода текста в английских и других западных языках размещается в левом верхнем углу текста). Однако первый аргумент DrawText представляет собой объект типа FormattedText, и с ним все оказывается гораздо сложнее. Более простой из двух его конструкторов вызывается с шестью аргу-
пользовательские элементы 201 ментами. Первый аргумент содержит текстовую строку, а остальные — характе- ристики выводимого текста. Один из аргументов, объект Typeface, создается сле- дующим образом: new Typeface(new FontFamlly("Times New Roman"). Fontstyles.Italic, FontWelghts.Normal, FontStretches.Normal) Или чуть проще: new Typeface("Times New Roman Italic"): Но объект FormattedText более гибок, чем может показаться при изучении его конструкторов. В частности, он позволяет применять разное форматирова- ние к разным частям текста. Методы, определяемые классом FormattedText (та- кие, как SetFontSize и SetFontStyle), существуют в версиях, позволяющих задать смещение и длину фрагмента, к которому применяется форматирование. Чтобы добавить небольшой текстовый фрагмент в метод BetterEllipse, вставьте следующий код в конец метода OnRender: FormattedText formtxt = new FormattedText("Hello, ellipse!", Cultureinfo.Currentculture, FlowDlrectlon, new Typeface("Times New Roman Italic"), 24, Brushes.DarkBlue); Point ptText = new Pol nt((RenderSIze.Width - formtxt.Width) / 2. (RenderSIze.Height - formtxt.Height) / 2); de.DrawText(formtxt, ptText): Для обращения к статическому свойству Cultureinfo.Currentculture потребуется директива using для пространства имен System.Globalization. FlowDirection является свойством класса FrameworkElement. Вычисления при создании ptText определя- ют положение левого верхнего угла текста, выровненного по центру эллипса. Конечно, этот код должен выполняться после вызова DrawEllipse. В противном случае эллипс будет нарисован поверх текста, и по крайней мере часть текста окажется скрыта. Но даже если текст находится на переднем плане, могут воз- никнуть проблемы, если задать эллипсу слишком малый размер: ellps.Width = 50: Вероятно, в этом случае текст выйдет за пределы эллипса. Как выясняется при выполнении этого кода, OnRender не отсекает текст по размерам RenderSize. Отсечение выполняется только в том случае, если какой-либо из размеров объ- екта sizeDesired, возвращаемого MeasureOverride, превышает соответствующий размер аргумента sizeAvailable. Если метод OnRender захочет предотвратить вы- ход графики за пределы RenderSize, он может установить область отсечения для Drawingcontext: de.PushCl1p(new RectangleGeometry(new Rect(new Po1nt(0, 0), RenderSize))); Но в действительности действовать следует иначе — определить FormattedText до или во время вызова MeasureOverride. В этом случае MeasureOverride сможет учесть размер текста при определении желаемого размера элемента.
202 Глава 10. Пользовательские элементы Конечно, измерение строки является лишь первым шагом на пути создания гипотетического класса «эллипса с встроенным текстом». Вероятно, вам также придется определить свойства не только для текста, но и для всех шрифтовых атрибутов, необходимых для FormattedText. Давайте попробуем разместить текст не в эллипсе, а в чем-нибудь более при- вычном — например, в кнопке. Для класса, производного от Control, доступны все шрифтовые свойства, определяемые классом Control, и они могут передавать- ся напрямую в конструкторе FormattedText. Далее приводится класс MedievalButton, производный от Control. Он предназна- чен для создания кнопок, на которых выводится текст. Класс включает свойст- во Text, при помощи которого программа задает текстовое содержимое кнопки, а в дополнение к нему создастся зависимое свойство TextProperty. Класс также определяет два маршрутизируемых события Knock и PreviewKnock, являющихся аналогами Click (код событий Knock и PreviewKnock, приведенный в главе 9, поза- имствован именно из этой программы). Хотя на первый взгляд класс выглядит весьма современно из-за реализа- ции зависимого свойства и маршрутизируемых событий ввода, я назвал кнопку «средневековой» (medieval), потому что вся прорисовка выполняется в методе OnRender. Как будет показано далее, для пользовательских элементов существу- ют и другие, более эффективные решения. MedievalButton.cs //------------------------------------------------ // Medieval Button.cs (с) 2006 by Charles Petzold //------------------------------------------------ using System; using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.GetMedleval { public class MedievalButton : Control { // Всего два приватных поля FormattedText formtxt; bool IsMouseReallyOver; // Статические поля, доступные только для чтения public static readonly Dependencyproperty TextProperty; public static readonly RoutedEvent KnockEvent; public static readonly RoutedEvent PrevlewKnockEvent;
Пользовательские элементы 203 // Статический конструктор static MedievalButtonO { // Регистрация зависимого свойства TextProperty = Dependencyproperty.Reglster("Text", typeof(strlng), typeof(MedlevalButton), new FrameworkPropertyMetadataC ", FrameworkPropertyMetadataOptlons.AffectsMeasure)); // Регистрация маршрутизируемых событий KnockEvent = EventManager.ReglsterRoutedEvent("Knock", Rout1ngStrategy.Bubble, typeof(RoutedEventHandler), typeof(MedievalButton)): PrevlewKnockEvent = EventManager.ReglsterRoutedEvent("PrevlewKnock", Routingstrategy.Tunnel, ypeof(RoutedEventHandler), typeof(MedlevalButton)): // Открытый интерфейс к зависимому свойству property. public string Text { set { SetValue(TextProperty, value == null ? " " : value): } get { return (strlng)GetValue(TextProperty): } } // Открытый интерфейс к маршрутизируемым событиям public event RoutedEventHandler Knock { add { AddHandler(KnockEvent, value): } remove { RemoveHandler(KnockEvent, value): } } public event RoutedEventHandler PreviewKnock { add { AddHandler(PrevlewKnockEvent, value): } remove { RemoveHandler(PrevlewKnockEvent, value): } } // Метод вызывается при возможном изменении размеров кнопки protected override Size MeasureOverr1de(S1ze sIzeAvallable) { formtxt = new FormattedText(Text, Cultureinfo.Currentculture, FlowDlrectlon, new Typeface(FontFamily, Fontstyle. FontWeight, FontStretch), FontSize, Foreground): // Внутренние отступы учитываются при вычислении размера Size sizeDesired = new S1ze(Math.Max(48, formtxt.Width) + 4,
204 Глава 10. Пользовательские элементы formtxt.Height + 4); sIzeDesIred.Width += Padding.Left + Padding.Right; sizeDesIred.Height += Padding.Top + Padding.Bottom: return sizeDesIred; } // Метод OnRender вызывается для перерисовки кнопки protected override void OnRender(Draw1ngContext de) { // Определение цвета фона Brush brushBackground = SystemColors.Control Brush; If (IsMouseReallyOver && IsMouseCaptured) brushBackground = SystemColors.ControlDarkBrush; // Определение ширины пера Pen pen = new Pen(Foreground, IsMouseOver ? 2 : 1); // Рисование прямоугольника с закругленными углами de.DrawRoundedRectanglе(brushBackground, pen, new Rect(new Po1nt(0, 0), RenderSize), 4, 4); // Определение основного цвета formtxt.SetForegroundBrush( IsEnabled ? Foreground : SystemColors.ControlDarkBrush); // Определение начальной точки текста Point ptText = new Po1nt(2, 2): switch (Ног1zonta1ContentAl1gnment) case Horizontal Allgnment.Left: ptText.X += Padding.Left: break; case Horizontal Allgnment.Right: ptText.X += RenderSize.Width - formtxt.Width - Padding.Right: break: case Horizontal Alignment.Center: case HorlzontalAl 1gnment.Stretch: ptText.X += (RenderSize.Width - formtxt.Width - Padding.Left - Padding.Right) / 2: break: } switch (Vert1 calContentAl1gnment) { case VerticalAlignment.Top: ptText.Y += Padding.Top; break: case VerticalAlignment.Bottom:
рппьзовательские элементы 205 ptText.Y += RenderSize.Height - formtxt.Height - Padding.Bottom; break; case VerticalAlignment.Center: case VerticalAlignment.Stretch: ptText.Y += (RenderSize.Height - formtxt.Height - Padding.Top - Padding.Bottom) / 2; break; } // Вывод текста de.DrawText(formtxt, ptText); } // События мыши, влияющие на внешний вид кнопки protected override void OnMouseEnter(MouseEventArgs args) { base.OnMouseEnter(args); InvalldateVlsualO; } protected override void OnMouseLeave(MouseEventArqs args) { base.OnMouseLeave(args): InvalldateVlsual(): } protected override void OnMouseMove(MouseEventArgs args) { base.OnMouseMove(args); // Проверка направления движения Point pt = args.GetPosltlon(thls): bool IsReallyOverNow = (pt.X >= 0 && pt.X < Actualwidth && pt.Y >= 0 && pt.Y < ActualHelght): If (IsReallyOverNow != IsMouseReallyOver) { IsMouseReallyOver = IsReallyOverNow; InvalldateVlsualO; } } protected override void OnMouseLeftButtonDown(MouseButtonEventArgs args) { base.OnMouseLeftButtonDown(args); CaptureMouse(); InvalldateVlsualO: args.Handled = true;
206 Глава 10. Пользовательские элементы // Событие, фактически инициирующее 'Knock' protected override void OnMouseLeftButtonllp(MouseButtonEventArgs args) { base.OnMouseLeftButtonUp(args): If (IsMouseCaptured) { if (isMouseReallyOver) { OnPreviewKnockO; OnKnock(); } args.Handled = true: Mouse.Capture(nul1); } } // При потере захвата мыши кнопка перерисовывается protected override void OnLostMouseCapture(MouseEventArgs args) { base.OnLostMouseCapture(args): InvalldateVlsual0: } // Клавиши "пробел" и Enter также вызывают // срабатывание кнопки protected override void OnKeyDown(KeyEventArgs args) { base.OnKeyDown(args): If (args.Key == Key.Space || args.Key == Key.Enter) args.Handled = true: } protected override void OriKeyUp(KeyEventArgs args) { base.OnKeyUp(args); if (args.Key == Key.Space || args.Key == Key.Enter) { OnPreviewKnockO; OnKnockO: args.Handled = true; } } // Метод OnKnock инициирует событие 'Knock' protected virtual void OnKnockO { RoutedEventArgs argsEvent = new RoutedEventArgsО: argsEvent.RoutedEvent = Medieval Button.PrevlewKnockEvent: argsEvent.Source = this: RalseEvent(argsEvent); }
Пользовательские элементы 207 // Метод OnPreviewKnock инициирует событие 'PreviewKnock' protected virtual void OnPreviewKnockО { RoutedEventArgs argsEvent = new RoutedEventArgs0; argsEvent.RoutedEvent = Medieval Button.KnockEvent; argsEvent.Source = this; RaiseEvent(argsEvent); } } } Несмотря на большое количество методов (в основном это переопределения методов, определенных в UIEIement и FrameworkElement), ничего принципиально нового в этом классе нет. Статический конструктор регистрирует зависимое свойство Text с событиями Knock и PreviewKnock, а открытые свойства и события определяются сразу же после статического конструктора. Метод MeasureOverride создает объект типа FormattedText и сохраняет его в поле. Почти все аргументы FormattedText — и конструктора Typeface в четвертом аргу- менте FormattedText — представляют собой свойства, определяемые Framework- Element, Control или самим классом MediavalButton (свойство Text). Конструктор FormattedText выглядит устрашающе, но в классе, производном от Control, пере- дать ему необходимую информацию не так уж сложно. Работа MeasureOverride завершается вычислением желательных размеров кноп- ки. Размеры определяются шириной и высотой отформатированного текста, плюс 4 единицы на обрамление. Чтобы кнопка не получилась слишком ма- ленькой из-за короткого текста, для нее устанавливается минимальная ширина в 0,5 дюйма. На последнем шаге MeasureOverride учитывается величина внутренних отступов (свойство Padding, определяемое классом Control). Метод MeasureOverride не должен пытаться учитывать свойства Margin, HorizontalAlignment или Vertical- Alignment данного элемента. Метод OnRender рисует кнопку. Для этого вызывается всего два метода: Draw- RoundedRectangle рисует контур и внутреннюю часть кнопки, a DrawText выводит текст внутри кнопки. Впрочем, перед вызовом этих методов необходимо проде- лать немалую подготовительную работу по определению цветов и местонахож- дения графических объектов. Когда кнопка была нажата, а указатель мыши все еще находится над кнопкой, фон кнопки должен слегка потемнеть. Кроме того, я решил, что когда указатель мыши оказывается над кнопкой, контур последней должен стать чуть толще. Обычно цвет текста определяется свойством Foreground, определяемым классом Control, но для заблокированной кнопки используется другой цвет. Две команды switch вычислят начальную позицию текста с учетом HorizontalContentAlignment, VerticalContentAlignment и Padding. Обратите внимание: объект ptText с именем Point инициализируется точкой (2, 2), чтобы зарезервировать место для контура вызовом Rectangle. Оставшаяся часть класса обеспечивает обработку событий ввода. В методах OnMouseEnter и OnMouseLeave достаточно вызвать InvalidateVisual для перерисовки
208 Глава 10. Пользовательские элементы кнопки. В исходной версии метод OnRender проверял, находится ли указатель мыши над кнопкой, при помощи свойства IsMouseOver. Однако при захвате мыши свойство IsMouseOver всегда истинно независимо от положения указателя мыши. Чтобы метод OnRender правильно выбирал цвет фона кнопки, в переопределе- нии OnMouseMove вычисляется значение поля, которое я назвал IsMouseReallyOver. В OnMouseLeftButtonDown класс захватывает мышь, запрашивает перерисовку кнопки и задает свойству Handled класса MouseButtonEventArgs значение true. По- следнее предотвращает дальнейшее восхождение события MouseLeftButtonDown по визуальному дереву и соответствует принципам обработки событий Click обыч- ным классом Button. Вызов OnMouseLeftButtonUp фактически инициирует события PreviewKnock и Knock (в указанном порядке), вызывая OnPreviewKnock и OnKnock. Я оформил два последних метода по образцу защищенного виртуального непараметризо- ванного метода OnClick, реализованного классом Button. Переопределения ОпКеу- Down и OnKeyUp инициируют события PreviewKnock и Knock, когда пользователь нажимает пробел или клавишу Enter. Программа GetMedieval создает экземпляр класса MedievalButton. GetMedieval.cs //-------------------------------------------- /7 GetMedieval.cs (с) 2006 by Charles Petzold //-------------------------------------------- using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input; using System.Windows.Media: namespace Petzold.GetMedieval { public class GetMedieval : Window [STAThread] public static void MainO { Application app = new Application(): app.Run(new GetMedieval0): } public GetMedieval() { Title = "Get Medieval": MedievalButton btn = new MedievalButtonO; btn.Text = "Click this button": btn.FontSize = 24: btn.Horizontal Alignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center:
пппьзовательские элементы 209 btn.Padding = new Thickness(5, 20, 5, 20): btn.Knock += ButtonOnKnock: Content = btn; void ButtonOnKnock(object sender, RoutedEventArgs args) { Medieval Button btn = args.Source as Medieval Button: MessageBox.Show("The button labeled \”" + btn.Text + “\” has been knocked.", Title): } } } Поэкспериментируйте co свойствами HorizontalAlignment, VerticalAlignment, Horiz- ontalContentAlignment, VerticalContentAlignment, Margin и Padding и убедитесь в том, что поведение кнопки имеет много общего с поведением обычного объекта Button. Но если явно задать MedievalButton очень малую ширину: btn.Width = 50: правая сторона кнопки усекается. Вероятно, будет лучше, если контур всегда будет отображаться на кнопке, независимо от ее размеров. Чтобы добиться же- лаемого эффекта, метод MeasureOverride может возвращать размер, всегда мень- ший или равный аргумента sizeAvailable: sizeDesired.Width = Math.Min(sizeDesired.Width, sizeAvailable.Width): sizeDesired.Height = Math.Min(sizeDesired.Height, sizeAvailable.Height): Метод OnRender выполняет отсечение только в том случае, если размеры в воз- вращаемом значении sizeDesired превышают соответствующие размеры sizeAvailable. (При использовании Math.Min размеры sizeDesired всегда будут меньше либо рав- ны размеров sizeAvailable.) Длинный текст, выходящий за границы кнопки, все равно будет отображаться. Чтобы форсировать применение отсечения, либо уве- личьте размеры sizeDesired на небольшую величину (даже 0,1 будет достаточно), либо вставьте в начало OnRender команду назначения области отсечения на ос- новании RenderSize: de.PushClip(new RectangleGeometry(new Rect(new Point(0, 0), RenderSize))); Предположим, вы решили использовать класс MedievalButton вместо Button, но иногда бывает нужно выделить курсивом одно-два слова в тексте кнопки. К сча- стью, класс FormattedText включает методы SetFontStyle, SetFontWeight и т. д., которые позволяют применить форматирование к подмножеству текста. Следователь- но, свойство Text объекта MedievalButton можно заменить свойством FormattedText и передать кнопке для отображения уже отформатированный объект FormattedText. Конечно, рано или поздно вам захочется вывести на MedievalButton растровое изображение. Класс DrawingContext содержит метод Drawlmage, получающий ар- гумент типа ImageSource (того же, который используется с элементом Image). А это значит, что вы можете добавить в MedievalButton новое свойство с именем
210 Глава 10. Пользовательские элементы ImageSource и включить в OnRender вызов Drawlmage. Но сначала необходимо решить, должна ли графика заменить текст пли выводиться вместе с ним, а так- же предусмотреть различные варианты их расположения. Конечно, есть и более удобный способ — обычный класс Button просто обла- дает свойством Content, которому можно задать любой объект. Заглянув в доку- ментацию Button и ButtonBase, вы увидите, что эти классы сами не переопределяют OnRender, а перепоручают непосредственное формирование изображения другим классам. Так, программа ExamineRoutedEvents из предыдущей главы подска- зывает, что кнопка Button в действительности состоит из объекта ButtonChrome и интерфейсного элемента, заданного в качестве содержимого кнопки (в данном примере TextBlock). Со структурной точки зрения классы SimpleEllipse, BetterEllipse и MedievalButton демонстрируют простейший вариант наследования от FrameworkElement или Control. Все, что потребуется от разработчика, — переопределить метод OnRender и, воз- можно, MeasureOverride. (Конечно, сами методы OnRender и MeasureOverride могут оказаться довольно сложными, но общая структура проста.) Тем не менее на практике часто используются дочерние элементы. Такой подход усложняет струк- туру элемента, но делает его более гибким и универсальным.
4 4 Элементы с одним XX дочерним объектом Многие классы, производные от FrameworkElement или Control, содержат дочер- ние элементы, для управления которыми класс обычно переопределяет одно свойство и четыре метода. 1. VisualChildrenCount — свойство, доступное только для чтения, определяется клас- сом Visual, от которого наследует UIEIement. Классы, производные от Frame- workElement, переопределяют это свойство, чтобы элемент мог задать количество своих дочерних элементов. Вероятно, переопределения VisualChildrenCount в ва- ших классах будут выглядеть примерно так: protected override Int VisualChildrenCount { get { } } 2. GetVisualChild — метод также определяется классом Visual. Параметр задает ин- декс от 0 до VisualChildrenCount-1. Класс должен переопределить этот метод, чтобы элемент возвращал дочерний объект, соответствующий индексу: protected override Visual GetVIsualChi 1d(1 nt Index) { } 3. MeasureOverride — с этим методом мы уже встречались. В нем элемент вычис- ляет и возвращает свой желательный размер: protected override Size Measure0verr1de(S1ze sizeAvailable) { return sIzeDesIred: }
212 Глава И. Элементы с одним дочерним объектом Однако элемент с дочерними элементами также должен принимать во вни- мание место, необходимое для отображения последних. Для этого он вызы- вает метод Measure для каждого дочернего объекта, после чего анализирует свойство DesiredSize. Открытый метод Measure определяется классом UIEIement. 4. ArrangeOverride — метод определяется FrameworkElement для замены метода ArrangeCore, определяемого UIEIement. Он получает объект Size, обозначающий окончательный макетный размер элемента. Во время выполнения Arrange- Override элемент размещает дочерние объекты на своей поверхности, вызывая для каждого метод Arrange (открытый метод, определяемый в UIEIement). Един- ственный аргумент Arrange содержит объект Rect, определяющий местоположе- ние и размер дочернего элемента относительно родителя. Метод ArrangeOverride обычно возвращает тот же объект Size, который был им получен: protected override Size Arrange0verr1de(S1ze sIzeFInal) { return sIzeFInal: 5. OnRender — метод, в котором элемент прорисовывает себя. Дочерние объекты прорисовывают себя в своих методах OnRender. Они располагаются поверх изображения, нарисованного элементом в своем методе OnRender: protected override void OnRender(DrawingContext de) { } Вызовы MeasureOverride, ArrangeOverride и OnRender происходят в строго опре- деленной последовательности. За вызовом MeasureOverride всегда следует вызов ArrangeOverride, за которым всегда следует вызов OnRender. Впрочем, последую- щие вызовы OnRender могут происходить без предшествующих вызовов Arrange- Override, а вызовы ArrangeOverride — без предшествующих вызовов MeasureOverride. В этой главе все внимание будет сосредоточено на элементах, обладающих одним дочерним объектом. Элементы с несколькими дочерними объектами обыч- но называются общим термином «панели»; они будут рассматриваться в сле- дующей главе. Класс EllipseWithChild наследует от класса BetterEllipse из главы 10 (просто что- бы избежать дублирования свойств Brush и Fill), но в него включается новое свойство Child типа UIEIement. EllipseWithChild.cs // // E111pseW1thCh11d.cs (с) 2006 by Charles Petzold // using System: using System.Windows:
Элементы с одним дочерним объектом 213 using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.EncloseElementlnEl1ipse ( public class El 1ipseWithChi1d : El 1i pseWithChi1d.csPetzold.RenderTheBetterEl11pse.BetterEl1i pse UIEIement child; // Открытое свойство Child public UIEIement Child { set { if (child != null) RemoveVisualChi 1d(chi1d): RemoveLog icalChild(child); } if ((child = value) != null) { AddVisualChild(chiId); AddLogicalChild(child); } } get { return child; } } // Переопределение VisualChildrenCount возвращает 1, // если свойство Child отлично от null. protected override int VisualChildrenCount { get { return Child != null ? 1 : 0; } } // Переопределение GetVisualChildren возвращает Child protected override Visual GetVisualChi 1d(int index) { if (index >0 || Child == null) throw new ArgumentOutOfRangeExceptionC'index");
214 Глава 11. Элементы с одним дочерним объектом return Child; } // Переопределение MeasureOverride вызывает метод // Measure дочернего объекта protected override Size MeasureOverride(Size sizeAvailable) { Size sizeDesired = new Size(0. 0); if (Stroke != null) sizeDesired.Width += 2 * Stroke.Thickness; sizeDesired.Height += 2 * Stroke.Thickness; sizeAvailable.Width = Math.Max(0, sizeAvailable.Width - 2 * Stroke.Thickness); sizeAvailable.Height = Math.Max(0, sizeAvailable.Height - 2 * Stroke.Thickness); } if (Child ! = null) Child.Measure(sizeAvailable); sizeDesired.Width += Child.DesiredSize.Width; sizeDesired.Height += Child.DesiredSize.Height; } return sizeDesired; } // Переопределение ArrangeOverride вызывает метод // Arrange дочернего объекта protected override Size Arrange0verride(S1ze sizeFinal) { if (Child != null) { Rect rect = new Rect( new Point((sizeFinal.Width - Child.DesiredSize.Width) / 2. (sizeFinal.Height - Child.DesiredSize.Height) / 2). Child.DesiredSize): Child.Arrange(rect); } return sizeFinal: } } } Для доступа к приватному полю child используется открытое свойство Child. Маловероятно, чтобы дочерний объект оказался производным от UIEIement, а не от FrameworkElement, однако на практике дочерние объекты принято считать объек- тами UIEIement и использовать именно этот тип при объявлении поля и свойства.
элементы с одним дочерним объектом 215 Метод доступа set свойства Child вызывает методы AddVisualChild/AddLogicalChild при задании элемента, отличного от null, и методы RemoveVisualChild/RemoveLogical- Child, если такой элемент был задан ранее. Класс, поддерживающий дочерние элементы, обязан обеспечить вызов этих методов для поддержания правильной структуры визуальных и логических элементов, необходимой для наследования свойств и маршрутизации событий. Переопределение GetVisualChild возвращает свойство Child и инициирует ис- ключение, если свойство недоступно. Но действительно ли необходимо переоп- ределять GetVisualChild? Казалось бы, поскольку метод доступа set свойства Child вызывает AddVisualChild, было бы разумно предположить, что реализация Get- VisualChild по умолчанию вернет именно этот элемент. Разумно или нет, но этот вариант не сработает. Любой класс, производный от FrameworkElement и под- держивающий собственную коллекцию дочерних объектов, обязан реализовать VisualChildrenCount и GetVisualChild. Как и в любом классе, производном от FrameworkElement, аргумент Measure- Override определяет доступный размер, который может лежать в интервале от О до бесконечности. Метод вычисляет желательный размер и возвращает его. В Ellip- seWithChild в начале метода объект sizeDesired имеет нулевые размеры, после чего к sizeDesired прибавляется удвоенная ширина контура эллипса. Так метод пока- зывает, что элемент намерен нарисовать по крайней мере весь контур эллипса. Затем ширина и высота вычитаются из аргумента sizeAvailable, но с логикой, пре- дотвращающей нулевые значения. Выполнение MeasureOverride продолжается вызовом Measure дочернего объек- та с отрегулированным значением sizeAvailable. В конечном итоге это приведет к вызову метода MeasureOverride дочернего объекта, а последний метод теорети- чески может вызвать методы Measure дочерних объектов следующего уровня. Так происходит измерение всего дерева дочерних объектов. Фактически метод Measure отвечает за обновление свойства DesiredSize элемента. Метод Measure дочернего объекта не возвращает значения, но MeasureOverride теперь может проанализировать свойство DesiredSize дочернего объекта и учесть его при определении собственного значения sizeDesired. Что собой представляет свойство DesiredSize? Конечно, инстинктивно хочет- ся связать его с возвращаемым значением MeasureOverride. Значения похожи, но не идентичны. Принципиальное различие заключается в том, что DesiredSize включает все внешние отступы Margin, заданные для элемента. Соответствую- щие вычисления будут описаны чуть позже. Метод ArrangeOverride дает элементу возможность разместить свои дочер- ние элементы на своей поверхности. Аргумент ArrangeOverride определяет оконча- тельный размер макета. Метод ArrangeOverride отвечает за вызовы метода Arrange всех своих дочерних объектов. Аргумент Rect метода Arrange обозначает место- положение и размер дочернего объекта: свойства Left и Тор задаются по отноше- нию к левому верхнему углу родителя. Значения Width и Height обычно вычита- ются из свойства DesiredSize дочернего объекта. Метод Arrange предполагает, что свойства Width и Height параметра Rect включают величину Margin дочернего объ- екта; именно поэтому для задания этих свойств используется DesiredSize.
216 Г лава 11. Элементы с одним дочерним объектом Метод Arrange дочернего объекта в конечном итоге вызовет метод Arrange- Override дочернего объекта с аргументом, не учитывающим Margin; теоретически это может привести к вызову методов Arrange дочерних объектов следующего уровня и т. д. Следующая программа создает объект EllipseWithChild и задает его свойству Child объект TextBlock. С таким же успехом свойству Child можно было бы задать объект Image или другой интерфейсный элемент. EncloseElementInEllipse.cs // // EncloseElementInElljpse.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.EncloseElementInEl1i pse public class EncloseElementlnEllipse : Window [STAThread] public static void MainO { Application app = new Application); app.Run(new EncloseElementInEl11pse()); } public Encl oseEl ement InEl lipseO { Title = "Enclose Element In Ellipse"; EllipseWithChild ellps = new El 11pseWithChi1d(); el Ips.Fl 11 = Brushes.ForestGreen; ellps.Stroke = new Pen(Brushes.Magenta. 48); Content = ellps; TextBlock text = new TextBlockO; text.FontFamily = new FontFamily("Times New Roman"); text.FontSize = 48; text.Text = "Text Inside ellipse"; // Свойству Child объекта EllipseWithChild // задается объект TextBlock el Ips.Chi Id = text; } } I
Элементы с одним дочерним объектом 217 Эксперименты с программой показывают, что элемент TextBlock всегда содер- жится в контуре эллипса — кроме случаев, когда эллипс настолько мал, что это приводит к нарушению логики MeasureOverride. Если для вывода текста не хвата- ет места, происходит отсечение. Вероятно, читатель уже понял, что экранный макет в WPF формируется за два этапа: процесс начинается с корневого элемента и проходит вниз по дереву элементов. Корневой элемент является объектом типа Window. Он обладает од- ним дочерним визуальным объектом типа Border (рамка изменения размеров окна). Последний обладает визуальным дочерним объектом типа Grid, который, в свою очередь, имеет два дочерних объекта: AdornerDecorator и ResizeGrip (нали- чие последнего не обязательно). AdornerDecorator обладает одним визуальным до- черним объектом типа Contentpresenter, отвечающим за представление свойства Content окна. В предыдущей программе единственным визуальным дочерним объектом Contentpresenter был объект EllipseWithChild, а его единственным визуаль- ным потомком — TextBlock. Каждый из этих элементов в методе MeasureOverride вызывает Measure для ка- ждого из своих дочерних объектов (чаще всего такой объект всего один). Метод Measure дочернего элемента вызывает метод MeasureOverride дочернего элемента, который вызывает Measure для дочерних объектов следующего уровня и т. д. Аналогичным образом организуются вызовы ArrangeOverride и Arrange. В общем случае интерфейсные элементы и элементы управления строятся из других элементов. Нередко структура элемента начинается с некоторой разновид- ности декоратора. Класс Decorator наследует от FrameworkElement и определяет свойство Child типа UIEIement. Например, класс Border, производный от Decorator, добавляет в него свойства Background, BorderBrush, BorderThickness, CornerRadius (для закругленных углов) и Padding. Класс ButtonChrome (из пространства имен Microsoft.Windows.Themes) также является производным от Decorator и обеспечи- вает внешний вид стандартных кнопок Button. Фактически Button состоит из объ- екта ButtonChrome и его дочернего объекта Contentpresenter. Далее приводится пример декоратора для кнопки с закругленными краями. Класс реализует свой метод OnRender на базе вызова DrawRoundedRectangle. Гори- зонтальная и вертикальная полуоси углов задаются равными половине высоты кнопки, так что левая и правая стороны кнопки имеют вид полуокружностей. RoundedButtonDecorator.cs // // RoundedButtonDecorator.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.CalculatelnHex
218 Глава 11. Элементы с одним дочерним объектом { public class RoundedButtonDecorator : Decorator { // Открытое зависимое свойство public static readonly Dependencyproperty IsPressedProperty; // Статический конструктор static RoundedButtonDecorator() { IsPressedProperty = DependencyProperty.ReglsterC'IsPressed". typeof(bool), typeof(RoundedButtonDecorator). new FrameworkPropertyMetadata(false, F rameworkPropertyMetadataOpt1ons.AffectsRender)); } // Открытое свойство public bool IsPressed set { SetValue(IsPressedProperty, value); } get { return (bool)GetValue(IsPressedProperty); } } // Переопределение MeasureOverride. protected override Size Measure0verr1de(S1ze sizeAvailable) { Size szDesired = new Size(2, 2); sizeAvailable.Width -= 2: sizeAvailable.Height -= 2; if (Child != null) { Chi Id.Measure(sizeAvailable): szDesired.Width += Child.DesiredSize.Width; szDesired.Height += Chi Id.DesiredSize.Height; } return szDesired; } // Переопределение ArrangeOverride protected override Size Arrange0verr1de(S1ze sIzeArrange) If (Child != null) { Point ptChild = new Po1nt(Math.Max(l, (sIzeArrange.Wldth - Child.DesiredSize.Width) / 2), Math.Maxd, (sIzeArrange.Helght - Child.DesiredSize.Height) / 2)); Child.Arrange(new Rect(ptCh11d, Child.DesiredSize));
Алименты с одним дочерним объектом 219 } return sizeArrange; } // Переопределение OnRender protected override void OnRender(Drawingcontext de) RadialGradientBrush brush = new RadialGradientBrush( IsPressed ? SystemColors.ControlDarkColor : SystemColors.Control LightLightColor, SystemColors.Control Col or); brush.Gradientorigin = IsPressed ? new Point(0.75, 0.75) : new Point(0.25. 0.25); de.DrawRoundedRectangle(brush, new Pen(SystemColors.ControlDarkDarkBrush, 1), new Rect(new Point(0, 0). RenderSize), RenderSize.Height / 2, RenderSize.Height / 2): } } } Класс не содержит явного определения свойства Child, потому что это свой- ство наследуется от Decorator. Класс определяет одно дополнительное свойс тво с именем IsPressed, используемое с радиальной градиентной кистью, созданной в методе OnRender. Чтобы обозначить нажатие кнопки, OnRender использует бо- лее темный центр и сдвигает начальную точку градиента. Класс RoundedButton, представленный далее, создает в конструкторе объект типа RoundedButtonDecorator, после чего вызывает AddVisualChild и AddLogicalChild, чтобы назначить декоратора дочерним объектом. RoundedButton определяет соб- ственное свойство Child, но просто передает заданный дочерний объект декора- тору. Переопределение VisualChildCount возвращает 1, a GetVisualChild возвращает RoundedButtonDecorator. Таким образом, классу RoundedButton вообще не нужно заниматься прорисов- кой; он отвечает только за обработку пользовательского ввода. Логика очень по- хожа на класс MedievalButton из главы 10, не считая, что RoundedButton определяет событие Click, сигнализирующее о нажатии кнопки. Свойство IsPressed в Rounded- Button предоставляет прямой доступ к тому же свойству RoundedButtonDecorator. Программа, использующая RoundedButton, может имитировать нажатия и отпус- кания кнопки посредством изменения этого свойства. RoundedButton.cs //---------------------------------------------- // RoundedButton.cs (с) 2006 by Charles Petzold //---------------------------------------------- using System; using System.Windows;
220 Глава 11. Элементы с одним дочерним объектом using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: namespace Petzold.CalculatelnHex { public class RoundedButton : Control { // Приватное поле RoundedButtonDecorator decorator: // Открытое статическое событие ClickEvent public static readonly RoutedEvent ClickEvent: // Статический конструктор static RoundedButton() { ClickEvent = EventManager.Regi sterRoutedEvent("Click", Routingstrategy.Bubbl e. typeof(RoutedEventHandler), typeof(RoundedButton)); } // Конструктор public RoundedButton() decorator = new RoundedButtonDecorator(): AddVisualChiId(decorator); AddLogicalChiId(decorator): } // Открытые свойства public UIEIement Child { set { decorator.ChiId = value: } get { return decorator.ChiId: } } public bool IsPressed { set { decorator.IsPressed = value: } get { return decorator.IsPressed: } } // Открытое событие public event RoutedEventHandler Click { add { AddHandler(ClickEvent. value): } remove { RemoveHandler(ClickEvent. value): } }
Элементы с одним дочерним объектом 221 // Переопределения свойства и методов protected override Int Visual ChiIdrenCount { get { return 1: } } protected override Visual GetVisualChild(int index) if (index > 0) throw new ArgumentOutOfRangeException("index"): return decorator; } protected override Size Measure0verride(S1ze sizeAvailable) decorator.Measure(sizeAvai1 able): return decorator.DesiredSize; } protected override Size ArrangeOverride(Size sizeArrange) decorator.Arrange(new Rect(new Point(0, 0), sizeArrange)): return sizeArrange; } protected override void OnMouseMove(MouseEventArgs args) base.OnMouseMove(args); if (IsMouseCaptured) IsPressed = IsMouseReallyOver; } protected override void OnMouseLeftButtonDown( MouseButtonEventArgs args) { base.OnMouseLeftButtonDown(args); CaptureMouseO; IsPressed = true; args.Handled = true; } protected override void OnMouseLeftButtonllp( MouseButtonEventArgs args) { base. OnMouseRi ght But tonllp (args); if (IsMouseCaptured) if (IsMouseReallyOver) OnClickO; Mouse.Capture(null); IsPressed = false; args.Handled = true; }
222 Глава 11. Элементы с одним дочерним объектом } bool IsMouseReallyOver { get { Point pt = Mouse.GetPosition(this); return (pt.X >= 0 && pt.X < ActualWidth && pt.Y >= 0 && pt.Y < ActualHeight); } } // Защищенный метод, инициирующий событие Click protected virtual void OnClickO { RoutedEventArgs argsEvent = new RoutedEventArgs(); argsEvent.RoutedEvent = RoundedButton.ClickEvent; argsEvent.Source = this; RaiseEvent(argsEvent): } } Программа CalculatelnHex создает 29 экземпляров RoundedButton для построе- ния шестнадцатеричного калькулятора. Кнопки размещаются на панели Grid, причем три из них распространяются на несколько столбцов, что требует осо- бой обработки в конструкторе окна. CalculateInHex.cs //------------------------------------------------ // CalculatelnHex.cs (с) 2006 by Charles Petzold //------------------------------------------------ using System: using System.Windows; using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: using System.Windows.Thread!ng: namespace Petzold.CalculatelnHex { public class CalculatelnHex : Window { // Приватные поля RoundedButton btnDisplay: ulong numDisplay: ulong numFirst: bool bNewNumber = true: char chOperation = ’=’;
япрменты с одним дочерним объектом 223 [STAThread] public static void MainO { Application app = new ApplicationO: app.Run(new CalculatelnHexO): } // Конструктор public CalculatelnHexO { Title = "Calculate in Hex": SizeToContent = SizeToContent.WidthAndHeight: ResizeMode = ResizeMode.CanMinimize; // Создание объекта Grid как содержимого окна Grid grid = new Grid(): grid.Margin = new Thickness(4): Content = grid: // Создание пяти столбцов for (int i = 0; i < 5; 1++) { ColumnDefinition col = new ColumnDefinition(); col.Width = GridLength.Auto; grid.ColumnDefinitions.Add(col); // Создание семи строк for (int i = 0: i < 7; i++) { RowDefinition row = new RowDefinitionO: row.Height = GridLength.Auto; grid.RowDefi ni ti ons.Add(row); } // Текст, выводимый на кнопках string[] strButtons = { "0", "D", и E" при "A", "B". "C", II _ II II у II "8", "9", ’4”. ”5”, "6", ”1”. "2", "3", . '1", "0". "Back II 1 'Equals int iRow =0, iCol = 0; // Создание кнопок foreach (string str in strButtons) { // Создание RoundedButton RoundedButton btn = new RoundedButtonO; btn.Focusable = false; btn.Height = 32;
224 Глава 11. Элементы с одним дочерним объектом btn.Margin = new Th1ckness(4); btn.Click += ButtonOnClIck; // Создание объекта TextBlock // для свойства Child объекта RoundedButton TextBlock txt = new TextBlockO; txt.Text = str; btn.Chi Id = txt; // Включение объекта RoundedButton на панель Grid grid.Children.Add(btn); Grid.SetRow(btn, iRow); Grid.SetColumn(btn, iCol); // Особая обработка для кнопки Display if (iRow == 0 && iCol == 0) { btnDisplay = btn; btn.Margin = new Thickness(4, 4. 4, 6); Grid.SetColumnSpan(btn, 5); iRow = 1; } // А также для кнопок Back и Equals else if (iRow == 6 && iCol >0) { Grid.SetColumnSpan(btn, 2); iCol += 2; } // For all other buttons, else { btn.Width = 32; if (0 == (iCol = (iCol + 1) X 5)) i Row++; } } } // Обработчик события Click void ButtonOnClick(object sender. RoutedEventArgs args) { // Получение кнопки, на которой был сделан щелчок RoundedButton btn = args.Source as RoundedButton; if (btn == null) return; // Получение текста кнопки и первого символа string strButton = (btn.Child as TextBlock).Text; char chButton = strButton[0];
кпрменты с одним дочерним объектом 225 // Некоторые особые случаи if (strButton == "Equals") chButton = if (btn == btnDisplay) numDisplay = 0; else if (strButton == "Back") numDisplay /= 16: // Шестнадцатеричные цифры else if (Char.IsLetterOrDigit(chButton)) { if (bNewNumber) { numFirst = numDisplay; numDisplay = 0; bNewNumber = false; if (numDisplay <= ulong.MaxValue » 4) numDisplay = 16 * numDisplay + (ulong)(chButton - (Char.IsDigit(chButton) ? 'O' ; 'A' - 10)); } // Рабочий режим else if (!bNewNumber) { switch (chOperation) case '='; break; case ' + ' ; numDisplay = numFirst + numDisplay; break; case 1 1 numDisplay = numFirst - numDisplay; break; case numDisplay = numFirst * numDisplay; break; case 1 &1; numDisplay = numFirst & numDisplay; break; case 111: numDisplay = numFirst | numDisplay; break; case 1 A ' . numDisplay = numFirst A numDisplay; break; case < ' numDisplay = numFirst « (int)numDisplay; break; case case numDisplay = numFirst : » (int)numDisplay; break; numDisplay = numDisplay != 0 ? numFirst / numDisplay ; ulong.MaxValue; break; case 'X': numDisplay = numDisplay != 0 ? numFirst % numDisplay : ulong.MaxValue; break; default; numDisplay = 0; break;
226 Глава 11. Элементы с одним дочерним объектом } } bNewNumber = true; chOperation = chButton; // Форматирование вывода TextBlock text = new TextBlockO; text.Text = String.Format("{0:X}", numDisplay); btnDisplay.Chi Id = text; } protected override void OnTextInput(TextCompositionEventArgs args) { base.OnTextlnput(args); if (args.Text.Length == 0) return; // Получение нажатой клавиши char chKey = Char.ToUpper(args.Text[0]); // Перебор кнопок foreach (UIEIement child in (Content as Grid).Children) { RoundedButton btn = child as RoundedButton; string strButton = (btn.Child as TextBlock).Text; // Messy logic to check for matching button. if ((chKey == strButton[0] && btn != btnDisplay && strButton != "Equals" && strButton != "Back") || (chKey == '=' && strButton == "Equals") || (chKey == '\r' && strButton == "Equals") || (chKey == '\b' && strButton == "Back") || (chKey == '\xlB' && btn == btnDisplay)) { // Имитация события Click для обработки нажатия клавиши RoutedEventArgs argsClick = new RoutedEventArgs(RoundedButton.ClickEvent. btn); btn.Rai seEvent(argsClick); // Имитация нажатия кнопки btn.IsPressed = true; // Установка таймера для возврата кнопки // в обычное состояние DispatcherTimer tmr = new DispatcherTimerO; tmr.Interval = TimeSpan.FromMilliseconds(lOO); tmr.Tag = btn; tmr.Tick += TimerOnTick;
дпрменты с одним дочерним объектом 227 tmr.Start О; args.Handled = true; } void TimerOnTick(object sender, EventArgs args) { // Отмена нажатия кнопки DispatcherTimer tmr = sender as DispatcherTimer; RoundedButton btn = tmr.Tag as RoundedButton; btn.IsPressed = false; // Остановка таймера и удаление обработчика события tmr.StopO; tmr.Tick -= TimerOnTick; } } } Большая часть логики калькулятора сосредоточена в методе ButtonOnClick, ис- пользуемом для обработки событий Click всех кнопок. Впрочем, кнопка также обладает клавиатурным интерфейсом, обработка которого производится в пе- реопределении OnTextlnput. Метод идентифицирует кнопку, соответствующую нажатой клавише, и инициирует событие Click этой кнопки вызовом метода Raise- Event объекта RoutedEventArgs. В завершение метод OnTextlnput имитирует нажа- тие кнопки установкой флага IsPressed. Конечно, в какой-то момент флаг необ- ходимо сбросить; и для этой цели в программе запускается таймер. Кроме переопределения FrameworkElement и Control, существует другой подход к построению пользовательских элементов, который чаще используется в коде XAML (а не в процедурном коде). Речь идет об определении шаблонов. Этот способ будет подробнее описан в главе 25, но сначала стоит посмотреть, как за- дача решается на уровне процедурного кода. В реализации этого решения ключевое место занимает свойство Template класса Control, относящееся к типу ControlTemplate. Фактически свойство Template определяет внешний вид и поведение элемента. Как говорилось выше, обычный объект Button состоит из двух объектов: ButtonChrome и Contentpresenter. Объект ButtonChrome обеспечивает вывод фона и рамки кнопки, a Contentpresenter отвеча- ет за управление объектом, заданным свойству Content кнопки. Шаблон определяет связи между этими объектами как триггеры, заставляю- щие элемент реагировать на некоторые изменения в свойствах элементов. Создание шаблона начинается с создания объекта типа ControlTemplate: ControlTemplate template = new ControlTemplateO; Класс ControlTemplate наследует свойство с именем VisualTree от FrameworkTem- plate. Это свойство определяет, из каких интерфейсных элементов состоит эле- мент управления. Вероятно, код построения шаблона покажется вам довольно странным, потому что он должен определять некоторые интерфейсные элементы
228 Глава 11. Элементы с одним дочерним объектом и их свойства без создания этих элементов. Задача решается при помощи клас- са, который называется FrameworkElementFactory. Объект FrameworkElementFactory создается для каждого элемента в визуальном дереве, например: FrameworkElementFactory factoryBorder = new FrameworkElementFactory(typeof(Border)); Для такого элемента также можно задать свойства и назначить обработчики событий. Для ссылок на свойства и события используются поля Dependencyproperty и RoutedEvent, определенные или унаследованные элементом. Пример задания свойства: factoryBorder.SetVa1ue(Border.BorderBrushProperty, Brushes.Red): В ControlTemplate также определяется свойство Triggers — коллекция объектов Trigger, описывающих изменение элемента в результате некоторых изменений свойств. Класс Trigger содержит свойство Property типа Dependencyproperty, опреде- ляющее отслеживаемое свойство, и свойство Value, определяющее его значение: Trigger trig = new TriggerO: trig.Property = UIEIement.IsMouseOverProperty; trig.Value = true: Когда свойство IsMouseOver становится истинным, происходит «нечто», опи- сываемое одним или несколькими объектами Setter. Например, свойство Fontstyle может переключиться на курсив: Setter set = new SetterO: set.Property = Control.FontStyleProperty: set.Value = Fontstyles.Italic: Объект Setter связывается с триггером посредством включения в коллекцию Setters последнего: trig.Setters.Add(set); А триггер включается в коллекцию триггеров ControlTemplate: template.Triggers.Add(trig); Теперь можно создать кнопку и назначить ей шаблон: Button btn = new ButtonO: btn.Tempi ate = template; Кнопка приобрела совершенно новый вид, но при этом по-прежнему генери- рует события Click. Следующая программа создает объект ControlTemplate для Button на уровне программного кода. BuildButtonFactory.cs // // BuildButtonFactory.cs (с) 2006 by Charles Petzold
алименты с одним дочерним объектом 229 using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: namespace Petzol d.Bui 1 dButtonFactory ( public class Bui 1dButtonFactory : Window { [STAThread] public static void MainO { Application app = new ApplicationO: app.Run(new Bui 1dButtonFactory!)): } public BulldButtonFactory!) { Title = "Build Button Factory": // Создание объекта Control Tempi ate для Button Control Tempi ate template = new ControlTemplate(typeof(Button)): // Создание объекта FrameworkElementFactory для Border FrameworkElementFactory factoryBorder = new FrameworkElementFactory(typeof(Border)): // Назначение имени для последующих ссылок factoryBorder.Name = "border": // Задание некоторых свойств по умолчанию factoryBorder.SetValue(Border.BorderBrushProperty, Brushes.Red): factoryBorder.SetValue(Border.BorderThicknessProperty, new Thickness(3)): factoryBorder.SetValue(Border.Backgroundproperty, SystemColors.ControlLightBrush); // Создание объекта FrameworkElementFactory для ContentPresenter FrameworkElementFactory factorycontent = new FrameworkElementFactory(typeof(ContentPresenter)); // Назначение имени для последующих ссылок factoryContent.Name = "content": // Привязка свойств ContentPresenter к свойствам Button factoryContent.SetValue(ContentPresenter.Contentproperty, new TempiateBi ndi ngExtensi on(Button.Contentproperty)):
230 Глава 11. Элементы с одним дочерним объектом // Обратите внимание: свойство Padding кнопки // соответствует свойству Margin содержимого! factorycontent.SetVa1ue(ContentPresenter.MarglnProperty, new TempiateBInd1ngExtensIon(Button.PaddlngProperty)); // Назначение Contentpresenter дочерним объектом Border factoryBorder.AppendChl1d(factorycontent): // Border назначается корневым узлом визуального дерева template.VIsualTree = factoryBorder; // Определение триггера для условия IsMouseOver=true Trigger trig = new TrlggerO; trig.Property = UIEIement.IsMouseOverProperty; trig.Value = true; // Связывание объекта Setter с триггером // для изменения свойства CornerRadlus элемента Border. Setter set = new SetterO; set.Property = Border.CornerRadlusProperty; set.Value = new CornerRad1us(24); set.TargetName = "border"; // Включение объекта Setter в коллекцию Setters триггера trig.Setters.Add(set); // Определение объекта Setter для изменения Fontstyle. // (Для свойства кнопки задавать TargetName не нужно.) set = new SetterO; set.Property = Control.FontStyleProperty; set.Value = Fontstyles.Italic; // Добавление в коллекцию Setters того же триггера trig.Setters.Add(set); // Включение триггера в шаблон tempi ate.Tr1ggers.Add(tr1g); // Определение триггера для IsPressed trig = new TrlggerO; trig.Property = Button.IsPressedProperty; trig.Value = true; set = new SetterO; set.Property = Border.BackgroundProperty; set.Value = SystemColors.Control DarkBrush; set.TargetName = "border";
Элементы с одним дочерним объектом 231 // Включение объекта Setter в коллекцию Setters триггера trig.Setters.Add(set): // Включение триггера в шаблон tempi ate.Tr1ggers.Add(tr1g); // Создание объекта Button Button btn = new ButtonO; // Назначение шаблона btn.Tempi ate = template; // Другие свойства определяются обычным образом btn.Content = "Button with Custom Template"; btn.Padding = new Thickness(20); btn.FontSize = 48; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.Click += ButtonOnClick; Content = btn; void ButtonOnClick(object sender, RoutedEventArgs args) { MessageBox.Show("You clicked the button", Title); } } } Свойство Template определяется классом Control, поэтому описанная методи- ка может применяться с пользовательскими элементами. Более того, как будет показано в примерах XAML из второй части книги, пользовательские элементы могут строиться исключительно на этой основе. Возможно, вы заметили, что методы Measure и Arrange определяются классом UIEIement, но методы VisualChildrenCount и GetVisualChild определяются классом Visual. Давайте еще раз рассмотрим иерархию классов от Object до Control: Object Dispatcherobject (абстрактный) Dependencyobject Visual (абстрактный) UIEIement FrameworkElement Control Дочерними объектами элемента обычно являются другие интерфейсные эле- менты, но некоторые из них (или все) могут быть визуальными объектами — эк- земплярами любого из потомков Visual. Визуальные объекты составляют визу- альное дерево.
232 Глава И. Элементы с одним дочерним объектом В следующей иерархии классов представлены все потомки Visual, за исключе- нием потомков FrameworkElement: Object Dispatcherobject (абстрактный) DependencyObject Visual (абстрактный) ContainerVisual DrawingVisual HostVisual Viewport3DVisual UIEIement FrameworkElement Особый интерес представляет класс DrawingVisual. Как говорилось ранее, класс, производный от UIEIement, может переопределить метод OnRender и получить объект DrawingContext для рисования графических объектов на экране. Единст- венным альтернативным источником для получения Drawingcontent может быть только объект DrawingVisual. Вот как это делается: DrawingVisual drawvis = new DrawingVisualО: DrawingContext de = drawvis.RenderOpen(); // Рисование на de dc.CloseO: После выполнения этого фрагмента вы становитесь обладателем объекта DrawingVisual — другими словами, визуального объекта, хранящего некоторое изображение. Параметры вызванных функций DrawingContext определили его ме- стоположение и размер. Местоположение задается относительно родительского элемента, который, возможно, еще не существует. Чтобы отобразить визуальный объект на экране, элемент должен обозначить существование дочернего визуаль- ного объекта в возвращаемых значениях VisualChildrenCount и GetVisualChild. Если вы хотите, чтобы визуальный объект участвовал в маршрутизации со- бытий (вероятно, это будет означать передачу событий мыши родительскому эле- менту), вызовите AddVisualChild для включения его в визуальное дерево элемента. Приведенный далее класс ColorCell наследует от FrameworkElement. Класс отве- чает за воспроизведение элемента, который всегда представляет собой квадрат со стороной в 20 аппаратно-независимых единиц. В центре элемента находится цветной квадрат со стороной в 12 единиц. Он является объектом типа Drawing- Visual, создаваемым в конструкторе класса (обратите внимание на параметр Color конструктора), и хранится в виде поля. ColorCell.cs // // ColorCell.cs (с) 2006 by Charles Petzold //
Элементы с одним дочерним объектом 233 using System; using System.Windows; using System.Windows.Media; namespace Petzold.Sei ectCol or ( class ColorCell ; FrameworkElement // Приватные поля static readonly Size sizeCell = new Size(20, 20); DrawingVisual visColor; Brush brush; // Зависимые свойства public static readonly Dependencyproperty IsSelectedProperty; public static readonly Dependencyproperty IsHighlightedProperty; static ColorCell0 { IsSelectedProperty = Dependencyproperty.RegisterCIsSelected". typeof(bool), typeof(ColorCell), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)); IsHighlightedProperty = Dependencyproperty.RegisterCIsHighlighted", typeof(bool), typeof(ColorCell), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)); } // Свойства public bool IsSelected set { SetValue(IsSelectedProperty, value); } get { return (bool)GetValue(IsSelectedProperty); } } public bool IsHighlighted set { SetValuedsHlghllghtedProperty, value); } get { return (bool)GetValue(IsH1ghlightedProperty); } } public Brush Brush { get { return brush; } } // Конструктор получает аргумент Color public ColorCell(Color clr) {
234 Глава И. Элементы с одним дочерним объектом // Создание нового объекта DrawingVisual // и его сохранение в поле. visColor = new DrawingVisualО; DrawingContext de = visColor.RenderOpen(); // Рисование прямоугольника заданного цвета Rect rect = new Rect(new Pointd), 0), slzeCell); rect.Inflate(-4. -4): Pen pen = new Pen(SystemColors.ControlTextBrush, 1): brush = new SolldColorBrush(clr): de.DrawRectangle(brush, pen. rect): dc.CloseO; // Вызов AddVIsualChi 1d необходим для маршрутизации событий AddVIsualChild(visColor): AddLogicalChild(visColor): // Переопределение защищенных свойств и методов // для визуального дочернего объекта protected override int VisualChi 1drenCount { get { return 1: } protected override Visual GetVIsualChild(1nt index) { if (index > 0) throw new ArgumentOutOfRangeExceptionC'Index"); return visColor; } // Переопределение защищенных методов определения размеров // и воспроизведения элемента protected override Size Measure0verride(S1ze sizeAvailable) { return slzeCell: } protected override void OnRender(Draw1ngContext de) { Rect rect = new Rect(new Po1nt(0, 0). RenderSize); rect.Inflate(-1. -1); Pen pen = new Pen(SystemColors.H1ghlightBrush, 1); If (IsHighlighted) dc.DrawRectangle(SystemColors.ControlDarkBrush. pen, rect);
?„аирнты С ОДНИМ дочерним объектом 235 else if (IsSelected) dc.DrawRectangle(SystemColors.ControlLightBrush, pen, rect); else de.DrawRectangle(Brushes.Transparent, null, rect); } } } Свойство VisualChildrenCount возвращает 1, чтобы обозначить присутствие ви- зуального объекта, a GetVisualChild возвращает сам объект. MeasureOverride просто возвращает размер всего элемента. OnRender выводит прямоугольник, находя- щийся под визуальным объектом. Сорок объектов ColorCell являются частью элемента, который я назвал ColorGrid. ColorGrid переопределяет метод VisualChildrenCount (возвращает 1) и метод GetVisual- Child (возвращает объект типа Border). К этому моменту свойству Child объекта Border задается панель UniformGrid, заполненная 40 разноцветными экземплярами ColorCell. ColorGrid.cs //------------------------------------------- // ColorGrid.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.Sei ectCol or { class ColorGrid : Control { // Количество строк и столбцов const int yNum = 5; const int xNum = 8; // Отображаемые цвета stringt.] strColors = new stringEyNum, xNum] { { "Black", "Brown", "DarkGreen", "MidnightBlue". "Navy". "DarkBlue", "Indigo", "DimGray" }. { "DarkRed", "OrangeRed", "Olive". "Green", "Teal", "Blue", "SlateGray", "Gray" }, { "Red", "Orange". "YellowGreen", "SeaGreen", "Aqua", "LightBlue", "Violet". "DarkGray" }.
236 Глава И. Элементы с одним дочерним объектом { "Pink", ’’Gold”, "Yellow”, "Lime”, "Turquoise". "SkyBlue", "Plum", "LightGray" }, { "LightPink", "Tan", "LightYellow", "LightGreen", "LightCyan", "LightSkyBlue", "Lavender", "White" } // Создаваемые объекты ColorCell ColorCell[,] cells = new ColorCell[yNum, xNum]; ColorCell cell Selected; ColorCell cel 1Highlighted; // Интерфейсные элементы, входящие в состав элемента управления Border bord: UniformGrid unigrid; // Текущий выбранный цвет Color clrSelected = Colors.Bl ack; // Открытое событие "Changed" public event EventHandler SelectedColorChanged; // Открытый конструктор public ColorGridO { // Создание объекта Border для элемента bord = new BorderO; bord.BorderBrush = SystemColors.ControlDarkDarkBrush; bord.BorderThickness = new Thickness(l); AddVisualChi 1d(bord); // Необходимо для маршрутизации событий AddLogi cal Chi 1d(bord); // Создание UniformGrid как дочернего объекта Border uni grid = new UniformGridO; uni grid.Background = SystemColors.WindowBrush; uni grid.Columns = xNum; bord.Chi Id = uni grid; // Заполнение панели UniformGrid объектами ColorCell for (int у = 0; у < yNum; y++) for (int x = 0; x < xNum; x++) { Color clr = (Color) typeof(Colors). GetProperty(strColors[y, x]).GetValue(nul1. null); cells[y, x] = new ColorCell(clr); unigrid.Chi 1dren.Add(cel1s[y. x]); if (clr == SelectedColor) {
Эпрменты с одним дочерним объектом 237 cell Selected = cel Is[у, x]; cells[y, x].IsSelected = true; } ToolTip tip = new ToolTipO; tip.Content = strColors[y, x]; cellsty. x],ToolTip = tip: } } // Открытое свойство SelectedColor (только чтение) public Color SelectedColor get { return clrSelected; } } // Переопределение Visual ChiIdrenCount protected override Int Visual ChiIdrenCount get { return 1; } } // Переопределение GetVIsualChi 1d protected override Visual GetVIsualChllddnt Index) { If (Index > 0) throw new Argument0ut0fRangeExcept1on("Index"): return bord; } // Переопределение MeasureOverride protected override Size Measure0verr1de(S1ze sizeAvailable) { bord.Measure(sizeAval 1 abl e); return bord.DesiredSize; } // Переопределение ArrangeOverrlde protected override Size Arrange0verr1de(S1ze sizeFinal) { bord.Arrange(new Rect(new Po1nt(0, 0), sizeFinal)); return sizeFinal; } // Обработка событий мыши protected override void OnMouseEnter(MouseEventArgs args) { base.OnMouseEnter(args); if (celIHighlighted != null)
238 Глава И. Элементы с одним дочерним объектом { cell Highlighted.IsHIghlтghted = false; cell Highlighted = null: } } protected override void OnMouseMove(MouseEventArgs args) { base.OnMouseMove(args); ColorCell cell = args.Source as ColorCell; if (cell != null) { if (celIHighlighted != null) celIHighlighted.IsHighlighted = false; celIHighlighted = cell; cel 1 Highlighted.IsHighlighted = true; } } protected override void OnMouseLeave(MouseEventArgs args) { base.OnMouseLeave(args): if (celIHighlighted != null) { cellHighlighted.IsHighlighted = false; celIHighlighted = null; } } protected override void OnMouseDown(MouseButtonEventArgs args) { base.OnMouseDown(args); ColorCell cell = args.Source as ColorCell; if (cell != null) { if (cellHighlighted != null) cellHighlighted.IsSelected = false: cellHighlighted = cell; cellHighlighted.IsSelected = true; FocusO; } protected override void OnMouseUp(MouseButtonEventArgs args) { ba se. OnMousellp( a rgs): ColorCell cell = args.Source as ColorCell; if (cell != null) { if (cellSelected != null) cell Selected.IsSelected = false; cellSelected = cell; cellSelected.IsSelected = true;
дпрменты с одним дочерним объектом 239 deselected = (cell Selected. Brush as SolidColorBrush).Color: OnSelectedColorChanged(EventArgs.Empty); } } // Обработка событий клавиатуры protected override void OnGotKeyboardFocus( KeyboardFocusChangedEventArgs args) { base.OnGotKeyboardFocus(args); if (cel 1 Highlighted == null) { if (cellSelected != null) cell Highlighted = cellSelected: else celIHighlighted = cellsEO. 0]: celIHighlighted.IsHighlighted = true: } } protected override void OnLostKeyboardFocus( KeyboardFocusChangedEventArgs args) { base.OnGotKeyboardFocus(args): if (cel 1 Highlighted != null) { cel 1 Highlighted.IsHighlighted = false; cel 1 Hi ghli ghted = null; } } protected override void 0nKeyDown(KeyEventArgs args) { base.OnKeyDown(args); int index = unigrid.Children.IndexOf(celIHighlighted): int у = index / xNum; int x = index % xNum; switch (args.Key) { case Key.Home: У = 0; x = 0; break; case Key.End: у = yNum - 1; x = xNum + 1; break; case Key.Down: if ((y = (y + 1) % yNum) == 0) x++: break;
240 Глава 11. Элементы с одним дочерним объектом case Key.Up: if ((у = (у + yNum - 1) % yNum) == yNum - 1) x--; break; case Key.Right: if ((x = (x + 1) % xNum) == 0) У++; break; case Key.Left: if ((x = (x + xNum - 1) % xNum) == xNum - 1) У--; break; case Key.Enter: case Key.Space: if (cellSelected != null) cellSelected.IsSelected = false; cell Selected = cell Highlighted: cellSelected.IsSelected = true; clrSelected = (cell Selected.Brush as SolidColorBrush).Color; OnSelectedColorChanged(EventArgs.Empty); break; default: return; if (x >= xNum || у >= yNum) MoveFocus(new Traversal Request( FocusNavi gati onDi recti on.Next)); else if (x < 0 || у < 0) MoveFocus(new Traversal Request( FocusNavigationDirecti on.Previous)); else { celIHighlighted.IsHighlIghted = false; celIHighlighted = cells[y. x]; celIHighlighted.IsHighlighted = true; } args.Handled = true; // Защищенный метод, инициирующий событие SeiectedColorChanged protected virtual void OnSelectedColorChanged(EventArgs args) { if (SeiectedColorChanged != null) SeiectedColorChanged(this. args);
элементы с одним дочерним объектом 241 В качестве прототипа ColorGrid использовалась палитра выбора цветов Micro- soft Word. Вероятно, вы видели ее в диалоговых окнах выбора цвета фона или шрифта. Одна ячейка палитры всегда является «выделенной»; она соответствует цвету, полученному с использованием свойства SelectedColor (я сделал это свойст- во доступным только для чтения, потому что элемент не может получать про- извольные цвета, отсутствующие в палитре). Выделенный цвет обозначается светлым фоновым прямоугольником. Для изменения выделенного цвета ис- пользуется мышь. Если элемент ColorGrid обладает фокусом ввода, в нем также имеется «под- свечиваемая» ячейка, обозначенная темным фоновым прямоугольником. Под- светка изменяется при помощи клавиш управления курсором. Чтобы подсвечен- ный цвет стал выделенным, следует нажать клавишу «пробел» или Enter. При каждом изменении выделенного цвета элемент вызывает метод OnSelec- tedColorChanged, который инициирует событие SelectedColorChanged. Следующая программа создает объект типа ColorGrid и размещает его между двумя фиктивными кнопками, которые показывают, как работает фокус ввода. Окно устанавливает обработчик события SelectedColorChanged и изменяет фон окна в зависимости от свойства SelectedColor элемента. SelectColor.cs и И Sei ectCol or.cs (с) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.Sei ectCol or public class SelectColor : Window { [STAThread] public static void MainO { Application app = new Application!); app.Run(new SelectColor!)); } public SelectColor!) { Title = "Select Color"; SizeToContent = SizeToContent.WidthAndHeight: // Создание объекта StackPanel как содержимого окна StackPanel stack = new StackPanel(); stack.Orientation = Orientation.Horizontal ; Content = stack;
242 Глава И. Элементы с одним дочерним объектом // Создание фиктивной кнопки для проверки передачи фокуса Button btn = new ButtonO: btn.Content = "Do-nothing button\nto test tabbing"; btn.Margin = new Th1ckness(24): btn.HorizontalAlIgnment = HorizontalAlignment.Center; btn.Vertical AlIgnment = Vertleal AlIgnment.Center; stack.Chi 1dren.Add(btn); // Создание элемента ColorGrld ColorGrld clrgrld = new ColorGrldO; clrgrld.Margin = new Th1ckness(24); clrgrld.HorlzontalAUgnment = HorizontalAlignment.Center; clrgrld.VerticalAlignment = VerticalAlignment.Center; clrgrld.SeiectedColorChanged += ColorGrldOnSelectedColorChanged; stack.Chi 1dren.Add(clrgrld); // Создание другой фиктивной кнопки btn = new ButtonO; btn.Content = "Do-nothing button\nto test tabbing"; btn.Margin = new Th1ckness(24); btn.HorizontalAlIgnment = HorizontalAlignment.Center; btn.Vertical AlIgnment = VerticalAlignment.Center; stack.Chi 1dren.Add(btn); } void ColorGr1d0nSelectedColorChanged(object sender. EventArgs args) { ColorGrld clrgrld = sender as ColorGrld; Background = new SolldColorBrush(clrgrld.SelectedColor); } } Полагаю, читатель уже знаком с элементом ListBox — элементом, предназна- ченным для вывода нескольких объектов (обычно текстовых строк) в вертикаль- ном списке и выбора одного из них пользователем. Вы не замечаете некоторого сходства между ColorGrid и ListBox? На первый взгляд между ними нет ничего об- щего, но попробуйте взглянуть на эти два элемента с более абстрактной точки зрения. Оба элемента предназначены для выбора одного объекта из готового списка. В главе 13 будет показано, как создать элемент, очень похожий на ColorGrid, на основе поддержки клавиатуры/мыши и логики выбора, встроенной в ListBox.
4 Пользовательские панели Одни потомки FrameworkElement обладают дочерними элементами, другие — нет. Ко второй категории принадлежат классы Image и все потомки Shape. Все по- томки FrameworkElement — в том числе все классы, производные от Contentcontrol и Decorator, — способны поддерживать один дочерний объект, хотя последний час- то может содержать дочерний объект. Класс, производный от FrameworkElement, также может поддерживать несколько дочерних объектов; важную категорию таких элементов составляют потомки Panel. Впрочем, наследование от Panel не является обязательным условием поддержки нескольких дочерних объектов. На- пример, InkCanvas является производным непосредственно от FrameworkElement и поддерживает коллекцию из нескольких дочерних элементов. В этой главе я покажу, как создавать классы, производные от Panel, и как обеспечить поддержку нескольких дочерних объектов без наследования от Panel. Вероятно, сравнение покажет, почему вариант с наследованием от Panel проще. Большим преимуществом Panel является наличие свойства Children для хранения дочерних объектов. Свойство относится к типу UIEIementCollection; коллекция берет на себя вызо- вы AddVisualChild, AddLogicalChild, RemoveVisualChild и RemoveLogicalChild при добав- лении или удалении дочерних объектов из коллекции. Класс UIEIementCollection способен на это, потому что он располагает информацией о родительском эле- менте. Единственному конструктору UIEIementCollection передаются два аргумента: визуального родителя типа UIEIement и логического родителя типа Framework- Element. Чаще всего эти два аргумента совпадают. Как сказано в документации Panel, класс Panel переопределяет VisualChildren- Count и GetVisualChild, избавляя вас от этой работы. Как правило, при наследова- нии от Panel не нужно и переопределять OnRender. Класс Panel определяет свойст- во Background, а в своем переопределении OnRender просто вызывает DrawRectangle для фоновой кисти. Остаются MeasureOverride и ArrangeOverride — два важнейших метода, которые необходимо реализовать в классе панели. В документации Panel приводятся неко- торые советы по реализации этих методов: для получения коллекции дочерних объектов рекомендуется использовать Internalchildren вместо Children. Свойство Internalchildren (также объект типа UIEIementCollection) включает все содержимое
244 Глава 12. Пользовательские панели коллекции Children, а также дочерние объекты, добавленные посредством при- вязки данных. Вероятно, простейшей разновидностью панелей является UniformGrid. Панель этого типа состоит из строк постоянной высоты и столбцов постоянной шири- ны. Чтобы дать представление о реализации панелей, приводимый далее класс UniformGridAlmost пытается имитировать функциональность UniformGrid. Класс определяет свойство Columns в сочетании с зависимым свойством, определяющим значение по умолчанию 1. Класс UniformGridAlmost не пытается рассчитать коли- чество столбцов и строк по количеству дочерних объектов — свойство Columns должно задаваться явно, а количество строк вычисляется на основании количе- ства дочерних объектов. Вычисленное значение Rows можно получить в виде свойства, доступного только для чтения. Кроме того, UniformGridAlmost не под- держивает свойства FirstColumn. UniformGridAlmost.cs // UniformGridAlmost.cs (с) 2006 by Charles Petzold using System; using System.Windows: using System.Windows.Input; using System.Windows.Controls; using System.Windows.Media; namespace Petzold.DuplicateUni formGrid { class UniformGridAlmost : Panel { // Открытые статические зависимые свойства только для чтения public static readonly Dependencyproperty ColumnsProperty; // Статический конструктор для создания зависимого свойства static UniformGridAlmost() { ColumnsProperty = Dependencyproperty.Regi ster( "Columns", typeof(int), typeof(UniformGridAlmost), new FrameworkPropertyMetadatad. FrameworkPropertyMetadataOptions.AffectsMeasure)); // Свойство Columns public int Columns { set { SetValue(ColumnsProperty, value); } get { return (int)GetValue(ColumnsProperty); } }
Пользовательские панели 245 // Свойство Rows, доступное только для чтения public int Rows { get { return (InternalChildren.Count + Columns - 1) / Columns; } // Переопределение MeasureOverride распределяет место protected override Size MeasureOverride(Size sizeAvailable) { // Вычисление размера дочернего элемента Size slzeChlld = new S1ze(s1zeAva11able.Width / Columns, sizeAvailable.Height / Rows); // Переменные для накопления максимальных ширин и высот double maxwidth = 0; double maxheight = 0: foreach (UIEIement child In Internalchildren) { // Вызвать Measure для каждого дочернего объекта... child.Measure(sizeChlId): // ... а затем проверить свойство DesiredSize // дочернего объекта maxwidth = Math.Max(maxw1dth. chi Id.DesiredSize.Width); maxheight = Math.Max(maxhe1ght, child.DesiredSize.Height); // Теперь вычисляется желательный размер для самой решетки return new S1ze(Columns * maxwidth, Rows * maxheight); } // Переопределение ArrangeOverrlde размещает дочерние объекты protected override Size Arrange0verr1de(S1ze sizeFinal) { // Вычисление размера дочерних объектов // для строк и столбцов одинакового размера Size slzeChlld = new SIze(sizeFInal.Width / Columns, sizeFinal.Height / Rows); for (Int Index = 0; Index < Internal Children.Count; 1ndex++) { Int row = Index / Columns; Int col = Index Я Columns; // Вычисление прямоугольника для каждого дочернего объекта // в границах sizeFinal... Rect rectChlId =
246 Глава 12. Пользовательские панели new Rect(new Po1nt(col * sizeChild.Width. row * slzeChlld.Height). sizeChild); // ... И вызов Arrange для этого объекта. Interna1 Chi 1dren[i ndex].Arrange(rectChiId); } return sIzeFInal: } } В MeasureOverride класс сначала вычисляет доступный размер для каждого дочернего объекта, предполагая, что область разделена на ячейки равных разме- ров (учтите, что один или оба из размеров sizeAvailable могут быть бесконечны- ми; в этом случае sizeChild тоже будет содержать один или два бесконечных раз- мера). Метод вызывает Measure для каждого дочернего объекта, а затем проверяет DesiredSize, накапливая максимальные значения ширины и высоты элементов. Метод ArrangeOverride предоставляет панели возможность расположить все дочерние объекты в решетке. По аргументу метода легко вычисляется ширина и высота ячейки, а также позиция каждого дочернего объекта. Метод вызывает Arrange для каждого дочернего объекта, после чего возвращает управление. Класс UniformGridAlmost и приведенный далее класс DuplicateUniformGrid состав- ляют проект DuplicateUniformGrid. DuplicateUniformGrid.cs // // DuplicatellniformGrid.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media: namespace Petzold.DuplicateUniformGrid public class DuplicateUniformGrid : Window [STAThread] public static void Main() { Application app = new Application(): app.Run(new DuplicateUniformGrid!)): } public DuplicatellniformGrid() { Title = "Duplicate Uniform Grid":
ПРпиэдвательские панели 247 // Создание объекта UniformGridAlmost как содержимого окна UniformGridAlmost uni grid = new UniformGridAlmost О: unigrid.Columns = 5; Content = uni grid; // Заполнение UniformGridAlmost кнопками случайного размера Random rand = new RandomO; for (int index = 0; index < 48; index++) { Button btn = new ButtonO; btn.Name = "Button" + index; btn.Content = btn.Name; btn.FontSize += rand.Next(10); unigrid.Chi 1dren.Add(btn); AddHandler(Button.Cli ckEvent. new RoutedEventHandler(ButtonOnClick)); } void ButtonOnCUck (object sender. RoutedEventArgs args) { Button btn = args.Source as Button; MessageBox.Show(btn.Name + " has been clicked", Title); } } Программа создает объект UniformGridAlmost с 5 столбцами и заполняет его 48 кнопками разных размеров. Некоторые эксперименты, которые я бы реко- мендовал опробовать: О Задайте свойству SizeToContent окна значение SizeToContent.WidthAndHeight. Раз- меры окна и панели подбираются таким образом, что размеры всех ячеек оп- ределяются размерами самой большой кнопки. О Задайте свойствам HorizontalAlignment и VerticalAlignment панели значение Center. Теперь окно сохраняет размер по умолчанию, а размер панели изменяется так, что размеры всех ячеек определяются размерами самой большой кнопки. О Задайте свойствам HorizontalAlignment и VerticalAlignment каждой кнопки значе- ние Center. Панель сохраняет те же размеры, что и клиентская область, все ячейки имеют одинаковые размеры, но размеры кнопок подбираются по раз- мерам их содержимого. О Задайте свойствам Height и Width панели значения, неподходящие для разме- щения всех кнопок. Часть содержимого панели отсекается. Другими словами, панель ведет себя так, как ожидалось. Найдите элемент ColorGrid из предыдущий главы и попробуйте заменить UniformGrid на Uniform- GridAlmost. Обычно именно при работе с панелями программисты впервые встречаются с концепцией присоединенных свойств. DockPanel, Grid и Canvas — все эти панели
248 Глава 12. Пользовательские панели обладают присоединенными свойствами. Как говорилось ранее, при добавле- нии элемента на панель Canvas также задаются значения двух присоединенных свойств, относящихся к элементу: canv.Children.Add(el): Canvas.SetLeft(el, 100); Canvas.SetTop(el. 150); При первом описании присоединенных свойств я упоминал, что они исполь- зуются панелями при размещении дочерних объектов. Пора разобраться, как это делается. Класс CanvasClone, показанный далее, очень похож на Canvas, однако он реа- лизует только присоединенные свойства Left и Тор, без Right и Bottom. CanvasClone.cs //------------------------------------------- // CanvasClone.cs (с) 2006 by Charles Petzold //------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.PalntOnCanvasClone { public class CanvasClone : Panel { // Определение двух зависимых свойств public static readonly Dependencyproperty LeftProperty; public static readonly Dependencyproperty TopProperty; static CanvasClone() { // Регистрация зависимых свойств как присоединенных // Значение по умолчанию равно 0. при любом изменении // размещение родителя становится недействительным LeftProperty = DependencyProperty.ReglsterAttachedCLeft", typeof(double). typeof(CanvasClone), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptlons.AffectsParentArrange)); TopProperty = DependencyProperty.ReglsterAttachedCTop". typeof(double), typeof(CanvasClone). new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptlons.AffectsParentArrange)); // Статические методы для чтения и записи присоединенных свойств public static void SetLeft(Dependencyobject obj, double value)
^зрительские панели 249 obj.SetVa1ue(LeftProperty, value); public static double GetLeft(DependencyObject obj) ( return (double)obj.GetValue(LeftProperty); public static void SetTop(DependencyObject obj. double value) obj.SetValue(TopProperty. value); public static double GetTop(DependencyObject obj) { return (double)obj.GetValue(TopProperty); } // Переопределение MeasureOverride просто вызывает Measure // для дочерних объектов protected override Size Measure0verr1de(S1ze sizeAvailable) { foreach (UIEIement child In Internalchildren) ch11d.Measure(new SIze(Double.PositiveInfinity, Double.Positiveinfinity)); // По умолчанию возвращается (0, 0). return base.MeasureOverrlde(sizeAval1able); } // Переопределение ArrangeOverride размещает дочерние объекты protected override Size Arrange0verr1de(S1ze sIzeFInal) { foreach (UIEIement child In Internalchildren) child.Arrange(new Rect( new Po1nt(GetLeft(ch11d), GetTop(chlld)), child.DesiredSize)); return sIzeFInal; } } Как видите, в классе нет членов с именем Left или Тор. Статические поля LeftProperty и TopProperty, доступные только для чтения, определяются в точности как обычные зависимые свойства и регистрируются в статическом конструкторе статическим методом DependencyProperty.RegisterAttached. Обратите особое внима- ние на параметр метаданных FrameworkPropertyMetadataOptions.AffectsParentArrange. Присоединенные свойства всегда присоединяются к дочернему объекту Canvas, а изменение свойства отражается на размещении родителя (то есть Canvas), так как родитель отвечает за размещение своих дочерних объектов. Похожий флаг AffectsParentMeasure используется в тех случаях, когда изменение присое- диненного свойства влияет не только на размещение, но и на размеры родителя.
250 Глава 12. Пользовательские панели Затем класс определяет четыре статических метода для чтения и записи при- соединенных свойств. Вероятно, при вызове этих методов в первом аргументе передается дочерний объект Canvas (или элемент, который скоро станет дочер- ним объектом). Обратите внимание: методы Set вызывают метод SetValue дочер- него объекта, чтобы свойство сохранялось в дочернем объекте. Вся работа MeasureOverride сводится к вызову Measure для каждого дочернего объекта с передачей бесконечных размеров. Такое поведение соответствует по- ведению Canvas. Обычно элементы, размещаемые на панели Canvas, имеют явно заданные или относительно четко фиксированные размеры (как, скажем, рас- тровые изображения или ломаные линии). Вызов Measure необходим для того, чтобы дочерние объекты могли вычислить свое свойство DesiredSize, используе- мое при выполнении ArrangeOverride. MeasureOverride возвращает значение из реа- лизации базового класса, то есть размер (0, 0). Метод ArrangeOverride просто вызывает Arrange для каждого из дочерних объ- ектов; при этом последний размещается в позиции, полученной методами GetLeft и GetTop. Эти методы вызывают GetValue дочерних объектов. (Здесь проявляет- ся интересное различие между классами, определяющими зависимые свойства, и классами, определяющими только присоединенные свойства. Класс, опреде- ляющий зависимые свойства, должен быть производным от Dependencyobject, потому что класс вызывает методы SetValue и GetValue, унаследованные от Depen- dencyObject. Однако класс, определяющий присоединенные свойства, не обязан быть производным от Dependencyobject, потому что ему не нужно вызывать ме- тоды SetValue и GetValue.) Следующая простая программа показывает, что CanvasClone действительно может размещать элементы на своей поверхности. PaintOnCanvasClone.cs // // Pa1ntOnCanvasClone.cs (с) 2006 by Charles Petzold // using System; using System.Windows: using System.Windows.Controls; using System.Windows.Input: using System.Windows.Media; using System.Windows.Shapes: namespace Petzold.Pa1ntOnCanvasClone { public class PaintOnCanvasClone : Window { [STAThread] public static void MalnO Application app = new Application); app.Run(new PalntOnCanvasCloneO):
Пользовательские панели 251 } public PalntOnCanvasClone() { Title = "Paint on Canvas Clone"; Canvas canv = new Canvas!): Content = canv; SolidColorBrush[] brushes = { Brushes.Red. Brushes.Green, Brushes.Blue }; for (int 1 = 0: 1 < brushes.Length: 1++) { Rectangle rect = new RectangleO; rect.Fl 11 = brushesfi]; rect.Width = 200: rect.Height = 200; canv.Chi 1dren.Add(rect): Canvas.SetLeft(rect, 100 * (i + 1)): Canvas.SetTop(rect, 100 * (i + D); } } } } Но чтобы по-настоящему опробовать класс CanvasClone, стоит включить его в программу CanvasClone из главы 9. Не забудьте заменить все упоминания Canvas в программе на Petzold.PaintOnCanvasClone.CanvasClone. Все методы про- граммы, содержащие упоминания присоединенных свойств Left и Тор, необходи- мо перевести на использование CanvasClone. К сожалению, при чтении или запи- си присоединенного свойства несуществующего класса не выводятся никакие сообщения об ошибках, но программа работает только при чтении и записи при- соединенных свойств CanvasClone. Если вы создаете класс с несколькими дочерними объектами, не являющийся производным от Panel, вероятно, в нем стоит определить свойство Children типа UIEIementCollection и свойство Background типа Brush. Тем самым вы приблизите свой класс к архитектуре Panel. С другой стороны, также будет поучительно со- здать панель прямым наследованием от FrameworkElement и реализацией пользова- тельской коллекции дочерних объектов, не относящейся к типу UIEIementCollection (хотя бы для того, чтобы получить представление о возникающих проблемах). Возникает соблазнительная мысль — начать реализацию коллекции дочер- них объектов с определения приватного поля: List<UIElement> children = new LIst<UIElement>(): А затем предоставить доступ к коллекции через открытое свойство: public List<UIElement> Children { } get { return children; }
252 Глава 12. Пользовательские панели Включение дочерних объектов в коллекцию производится так же, как и вклю- чение дочерних объектов в обычные панели: pnl.Children.Add(btn); Но такому решению кое-чего не хватает. Класс, определяющий поле children и свойство Children, не получает информации о добавлении или исключении объ- ектов из коллекции. Такая информация необходима классу для правильной орга- низации вызовов AddVisualChild, AddLogicalChild, RemoveVisualChild и RemoveLogicalChild. Одно из возможных решений основано на написании класса-аналога UIEIe- mentCollection, который бы выполнял эту работу самостоятельно или оповещал класс панели о появлении или исчезновении дочерних объектов. Также можно оставить простое приватное поле children и определить следую- щие методы: public void Add(UIElement el) { children.Add(el): AddVisualChi 1d(el): AddLogi cal Chi 1d(el): } public void Remove(UIEIement el) { children.Remove(el); RemoveVisualChild(el): RemoveLogicalChild(el): } Таким образом, для добавления дочерних элементов на панель вместо ко- манды вида pnl.Children.Add(btn): будет использоваться команда pnl.Add(btn): Решайте сами, сколько таких методов вам захочется написать. Конечно, ме- тод Add абсолютно необходим. Наличие методов Remove, IndexOf и Count, а так- же, возможно, индексатора не является обязательным и зависит от приложения. Следующий класс, производный от FrameworkElement, поддерживает собственную коллекцию дочерних объектов и размещает их по диагонали от левого верхнего к правому нижнему углу. DiagonalPanel.cs //----------------------------------------------- // Di agonal Button.cs (с) 2006 by Charles Petzold //----------------------------------------------- using System: usi ng System.Col 1ections.Generic:
Пользовательские панели 253 using System.Windows; using System.Windows.Controls; using System.Windows.Input: using System.Windows.Media; namespace Petzol d.Di agonaIi zeTheButtons ( class DiagonalPanel : FrameworkElement { // Приватная коллекция children List<UIElement> children = new List<UIElement>(); // Приватное поле Size sizeChildrenTotal; // Зависимое свойство public static readonly Dependencyproperty Backgroundproperty; // Статический конструктор для создания // зависимого свойства Background static DiagonalPanelО { Backgroundproperty = Dependencyproperty.Regi ster( "Background", typeof(Brush), typeof(DiagonalPanel), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender)); } // Свойство Background public Brush Background { set { SetValue(Backgroundproperty, value); } get { return (Brush)GetValue(BackgroundProperty); } } // Методы для обращения к коллекции дочерних объектов public void Add(UIEIement el) { children.Add(el); AddVisualChild(el): AddLogicalChi 1d(el): InvalidateMeasureO; } public void Remove!UIEIement el) { children.Remove(el); RemoveVisualChild(el);
254 Глава 12. Пользовательские панели RemoveLogical Chi 1d(el): InvalidateMeasureO; public int IndexOf(UIElement el) return children.IndexOf(el); // Переопределенные свойства и методы protected override int VisualChildrenCount { get { return chi 1dren.Count: } } protected override Visual GetVisualChild(1 nt index) { if (index >= children.Count) throw new ArgumentOutOfRangeExceptionCindex"); return children[index]; } protected override Size MeasureOverride(Size sizeAvailable) { sizeChiIdrenTotal = new Size(0, 0): foreach (UIEIement child in children) { // Вызов Measure для каждого дочернего объекта... child.Measure(new Size(Double.Positiveinfinity, Double.Posi ti velnfi ni ty)); // ... с последующей проверкой свойства DesiredSize sizeChiIdrenTotal.Width += child.DesiredSize.Width; sizeChiIdrenTotal.Height += child.DesiredSize.Height; } return sizeChiIdrenTotal; protected override Size ArrangeOverride(Size sizeFinal) { Point ptChild = new Point(0, 0); foreach (UIEIement child in children) { Size sizeChild = new Size(0, 0): sizeChiId.Width = chi Id.DesiredSize.Width * (sizeFinal.Width / sizeChiIdrenTotal.Width); sizeChild.Height = chi Id.DesiredSize.Height * (sizeFinal.Height / sizeChiIdrenTotal.Height); child.Arrange(new Rect(ptChild. sizeChild)); ptChild.X += sizeChild.Width: ptChild.Y += sizeChild.Height; } return sizeFinal;
Пользовательские панели 255 protected override void 0nRender(Draw1ngContext de) dc.DrawRectangle(Background, null, new Rect(new Po1nt(0, 0), RenderSize)); } } } Класс определяет свойство Background (в сочетании с зависимым свойством) и реализует OnRender простой закраской всей поверхности фоновой кистью. Класс реализует методы Add и Remove, а также метод IndexOf. Кроме того, он со- держит очень простые переопределения VisualChildrenCount и GetVisualChild. Я не сразу понял, какие значения Size следует передавать методам Measure до- черних объектов при выполнении MeasureOverride. Сначала я хотел действовать по аналогии с UniformGridAlmost: если панель содержит 7 дочерних объектов, то ар- 1умент Measure должен составлять 1/7 параметра sizeAvailable метода MeasureOverride. Но это не так: если один дочерний объект намного больше другого, ему должно достаться больше места. Тогда я решил передавать методу Measure каждого до- чернего объекта весь параметр sizeAvailable, но быстро понял, что и это ошибка. Каждый дочерний объект будет считать, что все пространство находится в его монопольном использовании, а это неверно. Оставался последний выход — пере- дать Measure аргумент Size с бесконечными размерами и дать дочернему объекту возможность определить свои размеры без каких-либо подсказок. Все происхо- дит точно так же, как если бы я создал Grid, задал каждому столбцу и строке ре- жим Auto, а затем разместил дочерние объекты в диагональных ячейках. Впрочем, в ArrangeOverride я решил действовать несколько иначе. Класс со- храняет в поле значение sizeChildrenTotal, вычисленное в MeasureOverride, а затем использует его — в сочетании с параметром sizeFinal метода ArrangeOverride и свой- ством DesiredSize каждого дочернего объекта — для масштабирования дочерних объектов по итоговым размерам панели. Помните: вызов ArrangeOverride всегда следует за вызовом MeasureOverride, поэтому все данные, вычисленные во вре- мя MeasureOverride (в частности, sizeChildrenTotal), могут позднее использоваться в ArrangeOverride. Программа DiagonalizeTheButtons создает панель типа DiagonalPanel и разме- щает на ней пять кнопок разных размеров. Панель всегда имеет размеры, доста- точные как минимум для размещения всех пяти кнопок, но если панель оказы- вается больше, то и кнопки соответственно увеличиваются. DiagonalizeTheButtons.cs //------------------------------------------------------- // D1agonalizeTheButtons.cs (с) 2006 by Charles Petzold И-------------------------------------------------------- using System; using System.Windows; using System.Windows.Controls: using System.Windows.Input;
256 Глава 12. Пользовательские панели using System.Windows.Media; namespace Petzold.Diagona1izeTheButtons { public class Diagonal!zeTheButtons : Window { [STAThread] public static void MainO { Application app = new Application): app.Run(new Diagona 1 izeTheButtons()): } public DiagonalizeTheButtons() { Title = "Diagonalize the Buttons"; DiagonalPanel pnl = new DiagonalPanel0; Content = pnl; Random rand = new RandomO; for (int i = 0; i < 5; i++) { Button btn = new ButtonO; btn.Content = "Button Number " + (i + 1); btn.FontSize += rand.Next(20); pnl.Add(btn); } } } } В последнем примере этой главы рассматривается радиальная панель — то есть панель, в которой дочерние элементы размещаются по кругу. Но прежде чем при- ступать к программированию такой панели, необходимо решить ряд концепту- альных вопросов. Круг можно условно разделить на секторы. Вероятно, самый прямолиней- ный подход к размещению элементов основан на размещении каждого элемента в отдельном секторе. Чтобы площадь сектора использовалась с максимальной эффективностью, элемент должен плотно прилегать к внешней границе сектора, как показано на рисунке.
Пользовательские панели 257 На рисунке сектор находится в правой части круга, поэтому элемент сохра- няет нормальную ориентацию. Конечно, это удобно, однако в других секторах для плотного прилегания к сектору элемент придется повернуть. Ширина и высота панели равна удвоенному радиусу R, а сумма всех углов а составляет 360°. При фиксированном размере дочернего элемента чем больше радиус, тем меньше угол. Существует два общих способа распределения места под дочерние объекты: если размер панели фиксирован, углы можно распреде- лять в зависимости от размера каждого элемента. Данный способ лучше всего подходит в том случае, если дочерние объекты сильно различаются по размерам. Но если ожидается, что размеры дочерних объектов будут примерно одина- ковыми, разумнее выделить всем элементам одинаковые углы. Таким образом, угол а будет равен 360°, разделенным на количество дочерних объектов. Ради- ус R вычисляется по максимальным размерам дочернего объекта. Именно этот путь я выбрал в своем примере. Радиус R достаточно легко вычисляется на основании фиксированного угла а, ширины (W) и высоты (Н) дочернего объекта. Начнем с построения отрезка А, который делит угол а пополам и проходит до внутреннего края дочернего объекта. Длина А вычисляется по следующей формуле: А = --НУ2 . tan(a/2) . В приведенной далее программе длина А называется innerEdgeFromCenter. Про- грамма также вычисляет значение outerEdgeFromCenter, равное сумме innerEdge- FromCenter и ширины элемента W.
258 Глава 12. Пользовательские панели Обратите внимание на другой радиус, проведенный от центра круга к право- му верхнему углу элемента. Его длина рассчитывается по теореме Пифагора: R = l(A + W)2 . На диаграммах элемент имеет почти квадратную форму, но у хмпогих элемен- тов (например, кнопок и текстовых блоков) ширина превосходит длину. И ши- рина и высота учитываются при вычислении радиуса, однако ориентация на диаграммах предполагает, что высота элемента является доминирующим разме- ром, а элементы размещаются на панели примерно так. Кнопки также можно расположить так, чтобы доминирующим размером была ширина.
плпьзовательские панели 259 Эти два режима, названные ByHeight и ByWidth, представлены двумя элемента- ми перечисления. RadialPanelOrientation.cs // Radi al PanelOrientation.cs (c) 2006 by Charles Petzold //-------------------------------------------------------- namespace Petzol d.Ci releTheButtons ( public enum RadialPanelOrientation { ByWidth, ByHeight } } Класс RadialPanel определяет свойство Orientation типа RadialPanelOrientation, до- полняемое зависимым свойством, обозначающим значение по умолчанию ByWidth (вторая из двух диаграмм). Класс является производным от Panel, поэтому он может использовать коллекции Children и Internalchildren, а также свойство Back- ground — ему и так предстоит проделать немало работы и от второстепенных ме- лочей лучше избавиться. Класс RadialPanel также определяет другое свойство с именем ShowPieLines — аналог свойства ShowGridLines класса Grid. Свойство предназначено исключитель- но для экспериментальных целей. RadialPanel.cs // // RadialPanel.cs (с) 2006 by Charles Petzold // using System: using System.Windows; using System.Windows.Controls: using System.Windows. Input: using System.Windows.Media: namespace Petzold.Ci releTheButtons { public class RadialPanel : Panel { // Зависимое свойство public static readonly Dependencyproperty OrientationProperty: // Приватные поля bool ShowPieLines; double angleEach: // угол для каждого дочернего объекта Size sizeLargest; // размер наибольшего дочернего объекта
260 Глава 12. Пользовательские панели double radius: // радиус круга double outerEdgeFromCenter; double InnerEdgeFromCenter: // Статический конструктор для создания // зависимого свойства Orientation static Radi al Panel() { OrientationProperty = Dependencyproperty.Register("Orientation", typeof(RadialPanelOrientation). typeof(Radial Panel), new FrameworkPropertyMetadata(Radi a1 Panel Ori entati on.ByWi dth, FrameworkPropertyMetadataOptions.AffectsMeasure)); } // Свойство Orientation public RadialPanelOrientation Orientation { set { SetValue(OrientationProperty, value): } get { return (RadialPanelOrientation)GetValue(OrientationProperty); } } // Свойство wPieLines public bool ShowPieLines { set if (value != showPieLines) InvalidateVisualO: showPieLines = value: } get { return showPieLines: } } // Переопределение MeasureOverride protected override Size MeasureOverride(Size sizeAvailable) { if (InternalChildren.Count == 0) return new Size(0, 0): angleEach = 360.0 / InternalChildren.Count: sizeLargest = new Size(0, 0): foreach (UIEIement child in Internalchildren) {
рппнзовательские панели 261 // Вызываем Measure для каждого дочернего объекта... child.Measure(new Size(Double.Positiveinfinity, Double.Positiveinfinity)); II ... после чего проверяем свойство DesiredSize sizeLargest.Width = Math. Max(sizel_argest. Width, chi 1d.Desi redSi ze.Wi dth); sizeLargest.Height = Math.Max(sizeLargest.Height, chi 1d.Desi redSi ze.Hei ght); } if (Orientation == RadialPanelOrientation.ByWidth) { // Вычисление расстояния от центра до краев элемента innerEdgeFromCenter = sizeLargest.Width 111 Math.Tan(Math.PI * angleEach / 360): outerEdgeFromCenter = innerEdgeFromCenter + sizeLargest.Height; // Вычисление радиуса круга по размерам // самого большого дочернего объекта radius = Math.Sqrt(Math.Pow(sizeLargest.Width /2,2)+ Math.Pow(outerEdgeFromCenter, 2)); } else { // Вычисление расстояния от центра до краев элемента innerEdgeFromCenter = sizeLargest.Height / 2 / Math.Tan(Math PI * angleEach / 360); outerEdgeFromCenter = innerEdgeFromCenter + sizeLargest.Width; // Вычисление радиуса круга по размерам // самого большого дочернего объекта radius = Math.Sqrt(Math.Pow(sizeLargest.Height / 2, 2) + Math.Pow(outerEdgeFromCenter, 2)); } // Возвращение размера круга return new Size(2 * radius, 2 * radius); } // Переопределение ArrangeOverride protected override Size ArrangeOverride(Size sIzeFInal) { double angleChild = 0: Point ptCenter = new Point(sizeFinal.Width / 2, sizeFinal.Height / 2); double multiplier = Math.Min(sizeFinal.Width / (2 * radius), sizeFinal.Height / (2 * radius)); foreach (UIEIement child in Internalchildren) {
262 Глава 12. Пользовательские панели // Сброс RenderTransform child.RenderTransform = Transform.Identity; if (Orientation == RadialPanelOrientation.ByWidth) // Размещение дочернего объекта наверху child.Arrange( new Rect(ptCenter.X - multiplier * sizeLargest.Width / 2. ptCenter.Y - multiplier * outerEdgeFromCenter. multiplier * sizeLargest.Width. multiplier * sizeLargest.Height)); } else // Размещение дочернего объекта справа child.Arrange( new Rect(ptCenter.X + multiplier * InnerEdgeFromCenter. ptCenter.Y - multiplier * sizeLargest.Height / 2, multiplier * sizeLargest.Width. multiplier * sizeLargest.Height)): } // Поворот дочернего объекта вокруг центра Point pt = TranslatePoint(ptCenter, child): child.RenderTransform = new RotateTransform(angleChild, pt.X. pt.Y): // Увеличение угла angleChild += angleEach: } return sizeFinal; } // Переопределение OnRender рисует границы секторов (по желанию) protected override void OnRender(DrawingContext de) { base.OnRender(dc): if (ShowPieLines) { Point ptCenter = new Pol nt(RenderSize.Width / 2. RenderSize.Height /2); double multiplier = Math.Min(RenderS1ze.Width / (2 * radius). RenderSize.Height / (2 * radius)): Pen pen = new Pen(SystemColors.W1ndowTextBrush. 1): pen.DashStyle = DashStyles.Dash: // Вывод круга dc.0rawE111pse(null. pen. ptCenter. multiplier * radius. multiplier * radius):
Пользовательские панели 263 // Инициализация угла double angleChiId = -angleEach / 2; if (Orientation == RadialPanelOrientation.ByWidth) angleChiId += 90; // Loop through each child to draw radial lines from center, foreach (UIEIement child in Internalchildren) dc.DrawLine(pen, ptCenter, new Point(ptCenter.X + multiplier * radius * Math.Cos(2 * Math.PI * angleChiId / 360), ptCenter.Y + multiplier * radius * Math.Sin(2 * Math.PI * angleChild / 360))): angleChild += angleEach; } } } } Метод MeasureOverride вычисляет angleEach простым делением 360 на общее количество дочерних объектов. Затем он перебирает все дочерние объекты и вы- зывает Measure с бесконечными размерами. Цикл foreach проверяет размеры всех дочерних объектов, определяя мак- симальную ширину и высоту. Полученные данные сохраняются в переменной sIzeLargestChild. Оставшаяся часть метода MeasureOverride зависит от ориентации. Метод вы- числяет два поля innerEdgeFromCenter и outerEdgeFromCenter, а также радиус кру- га. MeasureOverride возвращает размер квадрата со сторонами, равными удвоен- ному вычисленному радиусу. Работа ArrangeOverride начинается с вычисления центральной точки sizeFinal и коэффициента, который я назвал multiplier. Коэффициент применяется ко всем значениям, вычисленным в MeasureOverride (а конкретно, sizeLargest, innerEdgeFrom- Center и outerEdgeFromCenter), для расширения круга до размеров sizeFinal. Для ориентации RadialPanelOrientation.ByWidth метод Arrange размещает каждый дочер- ний объект в верхней части круга, а для ByHeight — в правой части. Затем метод RenderTransform поворачивает дочерний объект вокруг центра sizeFinal на angleChild градусов (обратите внимание: angleChild начинается с 0 и увеличивается на angle- Each для каждого дочернего объекта). Второй и третий аргументы RenderTransform обозначают положение центра sizeFinal относительно дочернего объекта. RenderTransform — одно из двух графических преобразований, поддерживае- мых FrameworkElement. Метод LayoutTransform был продемонстрирован в главе 3. Как следует из названия, вызов LayoutTransform приводит к изменению макета, потому что заставляет метод Measure вычислять другие значения DesiredSize. Предположим, элемент обычно вычисляет размеры DesiredSize 100 единиц в ши- рину на 25 единиц в высоту. Если настроить LayoutTransform для поворота на 90°, то размеры DesiredSize составят 25 единиц в ширину на 100 единиц в высоту.
264 Глава 12. Пользовательские панели С другой стороны, метод RenderTransform предназначен в основном для пре- образований, не изменяющих макета. Как сказано в документации, типичным применением RenderTransform является «анимация или применение временных эффектов к элементам». Конечно, класс RadialPanel использует RenderTransform для гораздо более серьезных целей, но несомненно и то, что на стадии макети- рования лучше держаться подальше от всего, что влияет на размеры элементов (в частности, от LayoutTransform). Следующая программа предназначена для экспериментов с RadialPanel. На пане- ли размещаются 10 кнопок слегка различающихся размеров в ориентации ByWidth. CircleTheButtons.cs //-------------------------------------------------- // CircleTheButtons.cs (с) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Windows: using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.Ci rcleTheButtons { public class CircleTheButtons : Window { [STAThread] public static void MainO { Application app = new Application); app.Run(new Ci rcleTheButtons()); } public CircleTheButtons() { Title = "Circle the Buttons"; RadialPanel pnl = new RadialPanel0; pnl.Orientation = RadialPanelOrientation.ByWidth; pnl.ShowPieLines = true; Content = pnl; Random rand = new RandomO; for (int i = 0; i < 10; i++) { Button btn = new ButtonO; btn.Content = "Button Number " + (1 + 1); btn.FontSize += rand.Next(10); pnl.Children.Add(btn);
Пользовательские панели 265 Благодаря применению коэффициента multiplier при увеличении панели по размерам контейнера происходит пропорциональное увеличение кнопок (чтобы кнопки не увеличивались, задайте multiplier равным 1). Также стоит провести ряд типичных экспериментов: например, если задать свойству SizeToContent окна зна- чение WidthAndHeight, или же задать свойствам HorizontalAlignment и VerticalAlignment панели значение, отличное от Stretch, каждая кнопка принимает размеры самой большой кнопки, а панель будет занимать минимально необходимое место. Так- же можно попытаться задать для каждой кнопки ненулевое свойство Margin или задать свойствам HorizontalAlignment и VerticalAlignment кнопок значение, отлич- ное от Stretch. И конечно, стоит попробовать выбрать для RadialPanel ориентацию ByWidth и изменить количество кнопок. Конечно, самой очевидной областью применения панелей является макети- рование обычных и диалоговых окон, но панели также играют свою роль в фор- мировании макетов элементов управлении (см. пример ColorGrid в предыдущей главе). Но давайте рассмотрим элемент более общего назначения — например, ListBox. ListBox начинается с объекта Border; объект Border содержит дочерний объект Scrollviewer; объект Scrollviewer содержит дочерний объект StackPanel, а объекты, перечисляемые в списке, являются дочерними объектами StackPanel. Как вы думаете, что произойдет, если бы StackPanel в ListBox удалось заменить панелью другого типа — скажем, RadialPanel? Объект ListBox будет выглядеть со- вершенно иначе, но работать будет более или менее так же. В следующей главе мы решим эту нетривиальную задачу и построим «радиальный список».
ListBox Многие элементы, встречавшиеся нам до настоящего момента (например, Window, Label, Button, Scrollviewer и ToolTip), наследовали от класса Contentcontrol. Все эти элементы обладают свойством Content, значением которого может быть тексто- вая строка или другой интерфейсный элемент. Свойству Content также можно задать объект панели с несколькими элементами. Объект GroupBox, обычно используемый для группировки переключателей, также наследует от Contentcontrol, но через промежуточный класс HeaderedCon- tentControl. Класс GroupBox содержит не только свойство Content, но и свойство Header для текста (или иного объекта), выводимого над полем. Элементы TextBox и RichTextBox не являются производными от Contentcontrol. Они наследуют от абстрактного класса TextBoxBase и предоставляют пользова- телю возможность редактировать и вводить текст. Класс ScrollBar тоже не яв- ляется производным от Contentcontrol — он наследует от абстрактного класса RangeBase, обладающего свойством Value типа double. Начиная с этой главы, мы рассмотрим другую важную ветвь иерархии Control: класс ItemsControl, наследующий непосредственно от Control. Элементы, производ- ные от ItemsControl, предназначены для вывода нескольких объектов (как прави- ло, однотипных) — в виде иерархии или списка. К их числу относятся меню, па- нели инструментов, строки состояния, иерархические деревья и списки. В этой главе основное внимание уделяется ListBox — одному из трех элемен- тов, производных от ItemsControl через класс Selector: Control ItemsControl Selector (абстрактный) ComboBox ListBox TabControl На абстрактном уровне объект ListBox позволяет пользователю выбрать один вариант (или, возможно, несколько вариантов) из набора. В своей стандартной форме он прост и незатейлив: варианты представлены в виде вертикального списка. Если список окажется слишком длинным (или варианты слишком ши- рокими), ListBox автоматически снабжается полосами прокрутки. Класс ListBox
267 UstBox обеспечивает цветовую пометку выделенного варианта, а также предоставляет интерфейс клавиатуры и мыши. Важнейшее свойство Items наследуется классом ListBox от ItemsControl. Свой- ство относится к типу ItemCollection, представляющему набор объектов типа object. Это подразумевает, что в элементах ListBox могут отображаться практически лю- бые объекты. Хотя для представления вариантов существует специальный класс ListBoxItem, пользоваться им не обязательно. В простейшей форме ListBox запол- няется строками. Объект ListBox создается традиционным способом: ListBox Istbox = new ListBoxO; Процесс включения вариантов в коллекцию Items называется «заполнением UstBox»: 1 stbox.Iterns.Add("Sunday"): 1stbox.Iterns.Add("Monday"): Istbox.Items.Add("Tuesday"); 1stbox.Iterns.Add("Wednesday"); Конечно, списки почти никогда не заполняются отдельными командами Add. Очень часто для заполнения используется массив: stringt] strDayNames = { "Sunday", "Monday", "Tuesday", "Wednesday". "Thursday", "Friday", "Saturday" }; foreach(string str in strDayNames) list.Items.Add(str): Другой способ заполнения ListBox основан на задании свойства ItemsSource, определяемого классом ItemsControl. Свойству ItemsSource задается практически любой объект, реализующий интерфейс lEnumerable; таким образом, значением этого свойства может быть любая коллекция, используемая с foreach. Пример: Istbox.ItemsSource = DateTimeFormatlnfo.Currentinfo.DayNames; Эти два подхода к заполнению ListBox являются взаимоисключающими. Свойства Items и ItemsSource определяются классом ItemsControl. Два других полезных свойства ListBox — Selectedlndex и Selectedltem — определяются классом Selector. Они обозначают текущий выделенный вариант, помеченный особым цве- том фона (по умолчанию — синим). По умолчанию свойство Selectedlndex равно -1, а свойство Selectedltem равно null (выделенный вариант отсутствует). ListBox не выделяет один из вариантов коллекции Items автоматически. Если выделенный вариант не будет явно выбран в программе, то оба свойства сохраняют значения по умолчанию, пока пользова- тель не получит доступ к элементу. Чтобы назначить выделенный вариант до того, как ListBox появится на экра- не, следует задать либо Selectedlndex, либо Selectedltem. Например, команда Istbox.Selectedltem = 5;
268 Глава 13. ListBox выделяет шестой элемент коллекции Items. Значение Selectedlndex должно быть целым числом в интервале от -1 до количества элементов в коллекции Items минус 1. При наличии выделенного варианта запись Istbox.Seiectedltem эквивалентна 1stbox.Iterns Е1 stbox.SeiectedIndex] При смене выделения ListBox инициирует событие SelectionChanged. Давайте создадим объект ListBox для выбора цвета — пример весьма баналь- ный, но бесспорно полезный. Как вам уже известно, класс Colors из пространства имен System.Windows.Media содержит 141 статическое свойство с названиями цветов, от AliceBlue до Yellow- Green. Статические свойства возвращают объекты типа Color. Мы можем опреде- лить следующий массив, а затем использовать его для заполнения списка: ColorEJ clrs = { Colors.AliceBlue. Colors.Antiquewhite, ... }; Но не торопитесь вводить 141 название цветов. Как было показано в главе 2, программа может воспользоваться механизмом рефлексии для получения всех объектов Color, определенных в Colors. Выражение typeof(Colors) возвращает объ- ект типа Туре. Метод GetProperties этого объекта возвращает массив объектов Propertyinfo, каждый из которых соответствует свойству класса Colors. Далее свойство Name класса Propertyinfo позволяет получить имя свойства, а метод Get- Value — значение, то есть цвет. Нашей конечной целью является отображение образцов цветов в списке ListBox. Но давайте для начала просто заполним список названиями цветов. ListColorNames.cs //------------------------------------------------ // ListColorNames.cs (с) 2006 by Charles Petzold //------------------------------------------------ using System: using System.Reflect!on; using System.Windows: using System.Windows.Controls: using System.Windows.Input; using System.Windows.Media: namespace Petzold.ListColorNames class ListColorNames : Window { [STAThread] public static void MainO { Application app = new ApplicationO: app.Run(new ListColorNamesO);
269 ListBox } public ListColorNamesO { Title = "List Color Names"; // Создание объекта ListBox ListBox Istbox = new ListBoxO; Istbox.Width = 150; Istbox.Height = 150; Istbox.SeiectionChanged += LIstBoxOnSelectionChanged; Content = Istbox; // Заполнение списка названиями цветов PropertyInfoLJ props = typeof(Colors).GetProperties(); foreach (Propertyinfo prop in props) 1stbox.Iterns.Add(prop.Name); } void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs args) { ListBox Istbox = sender as ListBox; string str = Istbox.Selectedltem as string; if (str != null) Color clr = (Color)typeof(Colors).GetProperty(str).GetValue(null, null); Background = new SolidColorBrush(clr); } } } } Программа не передает ListBox фокус ввода, поэтому изначально в списке от- сутствует выделенный вариант. Чтобы выделение появилось, достаточно щелк- нуть на одном из вариантов — фон окна окрашивается в выбранный цвет. Для прокрутки вариантов используется полоса прокрутки. Выделенный вариант все- гда отображается на особом фоне (по умолчанию — на синем). Также обратите внимание, что цвет текста выделенного варианта меняется с черного на белый для контраста с синим фоном. Конечно, все управление ListBox может осуществляться с клавиатуры. Запус- тите программу и нажмите клавишу Tab, чтобы передать ListBox фокус ввода. Вы увидите, что в первой строке списка появляется пунктирный прямоуголь- ник фокуса. В этот момент выделение все еще отсутствует. Клавиша «пробел» выделяет вариант, обозначенный пунктирным прямоугольником. Клавиши Т и Ф изменяют текущее выделение. Кроме того, объект ListBox реагирует на кла- виши Page Up, Page Down, Home и End. Если же ввести букву, список автоматиче- ски переходит к вариантам, начинающимся с указанной буквы.
270 Глава 13. ListBox Вернемся к программному коду. Конструктор окна создает объект ListBox и явно задает его размер в аппаратно-независимых единицах. Как правило, спискам назначается определенный размер, зависящий от содержимого списка и контекста. Если этого не сделать, элемент заполняет все выделенные для не- го места. Программа заполняет список названиями цветов, после чего назначает обра- ботчик события SelectionChanged. Для получения выделенного варианта обработ- чик использует свойство Selectedltem. (Обратите внимание на проверку значе- ния null, указывающего на отсутствие выделения.) В нашей программе ListBox заполняется текстовыми строками, которые необходимо преобразовать в объект Color. И снова на помощь приходит рефлексия: вызов GetProperty возвращает объект Propertyinfo, а вызов GetValue дает объект Color. Если вы предпочитаете, чтобы список обладал фокусом ввода и имел те- кущее выделение в начале работы программы, включите в конец конструктора следующий фрагмент: Istbox.Selectedltem = "Magenta": 1stbox.Scrol1IntoVj ew(1stbox.Seiectedltem): 1stbox.Focus(): Вызов ScrollIntoView прокручивает список так, чтобы в нем был виден выде- ленный вариант. Хотя в классе Selector реализована логика выделения только одного варианта, класс ListBox позволяет выделять сразу несколько вариантов. Свойству Selection- Mode задается одно из значений перечисления SelectionMode: Single, Extended или Multiple. В режиме SelectionMode.Multiple поддерживается возможность выделения и сброса отдельных вариантов щелчком левой кнопки мыши (также можно ис- пользовать клавиши со стрелками для перемещения рамки выделения и уста- навливать/снимать выделение клавишей «пробел»). В режиме SelectionMode.Extended выделение и сброс отдельных вариантов ле- вой кнопкой мыши осуществляется только при нажатой клавише Ctrl. Также можно выделить целый диапазон вариантов (Shift+щелчок). Если ни одна из клавиш Shift и Ctrl не удерживается, щелчок приводит к сбросу всего предыду- щего выделения. При работе с ListBox в режиме множественного выделения следует использо- вать свойство Selectedltems (обратите внимание на множественное число!). Хотя класс ListBox обладает необходимой поддержкой полей с множественным вы- делением, событие SelectionChanged, определяемое классом Selector, тоже может пригодиться. С событием передается объект типа SelectionChangedEventArgs с дву- мя свойствами Addedltems и Removedltems. Свойства содержат списки вариантов, добавляемых или удаляемых из коллекции выделенных вариантов. При работе с ListBox в режиме единичного выделения на эти свойства обычно можно не об- ращать внимания. Поскольку коллекция Items содержит объекты типа object, объекты Color мож- но заносить прямо в ListBox. Давайте посмотрим, как это делается.
271 ListBox LlstColorValues.cs И ListColorValues.es (с) 2006 by Charles Petzold // using System; using System.Reflection: using System.Windows; using System.Windows.Input; using System.Windows.Controls; using System.Windows.Media; namespace Petzold.Li stColorVa1ues ( class ListColorValues : Window { [STAThread] public static void Main!) { Application app = new ApplicationO; app.Run(new LlstColorValuesO); } public ListColorValues!) { Title = "List Color Values"; // Создание объекта ListBox как содержимого окна ListBox Istbox = new LIstBoxO; Istbox.Width = 150; Istbox.Height = 150; Istbox.SeiectlonChanged += LIstBoxOnSelectlonChanged; Content = Istbox; // Заполнение ListBox объектами Color Propertylnfo[] props = typeof(Colors).GetPropert1es(); foreach (Propertyinfo prop In props) Istbox.Items.Add(prop.GetValue(null, null)); } void L1stBoxOnSelect1onChanged(object sender, SelectlonChangedEventArgs args) ListBox Istbox = sender as ListBox; If (Istbox.Seiectedlndex != -1) Color clr = (Color)lstbox.Selectedltem; Background = new SolldColorBrush(clr); } } }
272 Глава 13. ListBox Такое решение упрощает обработчик события, потому что Selectedltem доста- точно преобразовать к типу Color. В остальном оно просто ужасно: в ListBox вы- водятся строки, возвращаемые методом ToString класса Color, а эти строки имеют вид #FFF0F8FF. Даже заядлому программисту вряд ли захочется видеть шестна- дцатеричные коды цветов в списках. Проблема заключается в том, что структура Color ничего не знает о названиях из класса Colors. Меня бы нисколько не удиви- ло, если бы вся реализация свойств класса Colors выглядела примерно так: public static Color AliceBlue { get { return Color.FromRgb(0xF0, 0xF8, OxFF); } } Впрочем, это только мои предположения. Color — не самый подходящий тип объекта для размещения в ListBox. Для этой цели гораздо лучше подходят объекты с методом ToString, возвращающим именно ту информацию, которая должна отображаться в списке. Конечно, такое возможно не всегда, а кто-то скажет, что строковое представление, возвращае- мое методом ToString, прежде всего, должно форматироваться для удобства ком- пьютерной обработки. Как бы то ни было, в примере с Color придется внести ос- новательные изменения, чтобы метод ToString возвращал название цвета вместо шестнадцатеричного кода. Класс NamedColor демонстрирует более подходящее определение класса, объ- екты которого используются для заполнения ListBox. NamedColor.cs // // NamedColor.cs (с) 2006 by Charles Petzold // using System: using System.Reflection; using System.Windows.Media; namespace Petzold.ListNamedColors { class NamedColor { static NamedColor[] nclrs; Color clr; string str: // Статический конструктор static NamedColorO { Propertylnfo[] props = typeof(Colors).GetProperties(); nclrs = new NamedColor[props.Length]; for (int i = 0; i < props.Length; i++) nclrsEi] = new NamedColor(props[i].Name,
273 UstBox (Col or)props[1].GetValue(nul1. null)): } // Приватный конструктор private NamedColor(string str, Color clr) { this.str = str: this.clr = clr: // Статическое свойство, доступное только для чтения public static NamedColor[] All { get { return nclrs: } // Свойства, доступные только для чтения public Color Color { get { return clr; } } public string Name get { string strSpaced = str[0].ToString(); for (int i = 1; i < str.Length; i++) strSpaced += (char. Isllpper(str[i]) ? " " : "") + str[i].ToString(); return strSpaced; } } // Переопределение метода ToString public override string ToStringO { return str; } } } Класс NamedColor содержит два приватных поля экземпляров с именами clr и str. В этих полях хранится значение Color и строка с именем цвета (например, «AliceBlue»). Значения полей задаются в приватном конструкторе NamedColor. Обращения к полям производятся из открытого свойства Color и из переопреде- ления свойства ToString. Свойство Name сходно с ToString, если не считать того, что названия цветов разделяются пробелами для удобства чтения — например, «AliceBlue» превра- щается в «Alice Blue».
274 Глава 13. ListBox Статический конструктор играет важнейшую роль в функциональности клас- са. Он обращается к классу Colors через механизм рефлексии. Для каждого свой- ства Colors статический конструктор NamedColor создает объект типа NamedColor приватным конструктором экземпляра. Полученный массив объектов NamedColor сохраняется в поле, доступном через статический метод АН. Код заполнения ListBox объектами NamedColor выглядит совсем просто: foreach (NamedColor nclr in NamedColor.All) Istbox.Items.Add(nclr); ListBox использует строку, возвращаемую ToString, для вывода информации. Обработчик события преобразует Selectedltem в объект типа NamedColor, после чего обращается к свойству Color: Color clr = (Istbox.Selectedltem as NamedColor).Color; Такое решение работает, но существует более простая альтернатива. Вместо того чтобы заполнять ListBox в цикле, можно занести массив объектов NamedColor прямо в свойство ItemsSource объекта ListBox: Istbox.ItemsSource = NamedColor.All; Команда полностью заменяет цикл foreach, а программа работает точно так же. Независимо от способа заполнения, все варианты ListBox относятся к одному типу NamedColor. Это обстоятельство позволяет использовать три других свойст- ва ListBox: DisplayMemberPath, SelectedValuePath и SelectedValue. Свойство DisplayMemberPath определяется классом ItemsControl. Ему задается строка с именем свойства, которое должно использоваться ListBox для отображе- ния данных. Для вариантов типа NamedColor это свойство Name: Istbox.DisplayMemberPath = "Name"; Теперь объект ListBox для отображения вариантов будет использовать вместо ToString свойство Name каждого варианта. Два других свойства определяются классом Selector. Свойство SelectedValue- Path обозначает имя свойства, представляющего значение варианта. Для класса NamedColor это свойство Color, поэтому значение SelectedValuePath может задавать- ся следующим образом: Istbox.SelectedValuePath = "Color"; Далее для получения текущего варианта в обработчике события вместо Selec- tedlndex или Selectedltem можно использовать SelectedValue. Свойство SelectedValue возвращает свойство Color выделенного варианта. Преобразование типа по-преж- нему необходимо, но код определенно упрощается: Color clr = (Color) Istbox.SelectedValue; Итак, строки, задаваемые DisplayMemberPath и SelectedValuePath, должны содер- жать имена свойств, определяемых классом (или структурой), соответствующим типу вариантов ListBox. DisplayMemberPath сообщает, какое свойство должно ис- пользоваться для отображения вариантов ListBox, a SelectedValuePath указывает, какое свойство используется для возвращаемого значения SelectedValue.
275 ListBox Далее приведен пример полной программы, использующей класс NamedColor для заполнения ListBox. UstNamedColors.cs // ListNamedColors.cs (с) 2006 by Charles Petzold // using System: using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ListNamedColors class ListNamedColors : Window t [STAThread] public static void Main() { Application app = new Application!); app.Run(new ListNamedColors!)): } public ListNamedColors() { Title = "List Named Colors"; // Создание ListBox как содержимого окна ListBox Istbox = new ListBoxO; Istbox.Width = 150; Istbox.Height = 150; Istbox.SeiectionChanged += ListBoxOnSelectionChanged; Content = Istbox; // Заполнение списка и задание свойств Istbox.ItemsSource = NamedColor.All; Istbox.DisplayMemberPath = "Name"; Istbox.SelectedValuePath = "Color"; } void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs args) { ListBox Istbox = sender as ListBox; if (Istbox.SelectedValue != null) { Color clr = (Color)lstbox.SelectedValue; Background = new SolidColorBrush(clr);
276 Глава 13. ListBox Как видите, благодаря использованию класса объем программного кода за- метно сократился. Но нельзя ли сделать его еще более компактным? Заметьте, что ListBox возвращает SelectedValue типа Color, тогда как для зада- ния свойства Background окна нам нужен объект типа Brush. Если бы в ListBox хра- нились объекты со свойством Brush, мы могли бы связать свойство SelectedValue со свойством Background окна. Давайте попробуем это сделать. Следующий класс почти идентичен NamedColor, если не считать того, что все ссылки на Color и Colors заменяются ссылками на Brush и Brushes. NamedBrush.cs // // NamedBrush.cs (с) 2006 by Charles Petzold // using System; using System.Reflection; using System.Windows.Media; namespace Petzold.ListNamedBrushes { public class NamedBrush { static NamedBrush[] nbrushes; Brush brush; string str; // Статический конструктор static NamedBrushO { Propertylnfo[] props = typeof(Brushes).GetProperties(); nbrushes = new NaniedBrushtprops.Length]; for (int i = 0; i < props.Length; i++) nbrushes[i] = new NamedBrush(props[i].Name, (Brush)props[i].GetValue(null, null)); } // Приватный конструктор private NamedBrush(string str, Brush brush) { this.str = str; this.brush = brush; } // Статическое свойство, доступное только для чтения public static NamedBrush[] All { get { return nbrushes: } }
277 ListBox // Свойства, доступные только для чтения public Brush Brush { get { return brush: } } public string Name { get { string strSpaced = str[0].ToString(): for (int i = 1: i < str.Length: i++) strSpaced += (char.Isllpper(str[i]) ? " " : "") + strQi].ToString(): return strSpaced: } } // Переопределение метода ToString public override string ToString() { return str: } } } При занесении в ListBox объектов NamedBrush свойство SelectedValuePath зада- ется равным «Brush». Если вы предпочитаете использовать обработчик события, свойство Background можно задать по SelectedValue с преобразованием типа: Background = (Brush)lstbox.SelectedValue; Впрочем, обработчик можно вообще убрать и заменить его привязкой данных: 1stbox.SetBl nd1ng(LIstBox.SeiectedValueProperty, "Background"); Istbox.DataContext = this; Привязка показывает, что свойство SelectedValue объекта ListBox связывается со свойством Background объекта this (окна). Пример использования NamedBrush. UstNamedBrushes.cs // // L1stNamedBrushes.cs (с) 2006 by Charles Petzold И--------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input: using System.Windows.Media; namespace Petzold.LIstNamedBrushes
278 Глава 13. ListBox { public class ListNamedBrushes : Window [STAThread] public static void MainO { Application app = new Application): app.Run(new ListNamedBrushes()): } public ListNamedBrushes0 Title = "List Named Brushes"; // Создание объекта ListBox как содержимого окна ListBox Istbox = new ListBoxO; Istbox.Width = 150; Istbox.Height = 150; Content = Istbox; // Заполнение списка и задание свойств Istbox.ItemsSource = NamedBrush.All; Istbox.DisplayMemberPath = "Name"; Istbox.SeiectedValuePath = "Brush"; // Привязка SelectedValue к свойству Background окна 1stbox.SetBinding(ListBox.SeiectedValueProperty, "Background"); Istbox.DataContext = this; } } } Программа хороша тем, что в ней нет обработчиков событий. Обработчики всегда казались мне чем-то вроде подвижных частей механизма. Избавившись от них, вы избавляетесь от потенциальных проблем (конечно, за кулисами «под- вижные части» остаются, но до них нам уже нет дела). Что произойдет, если ListBox находится в состоянии без выделенного вариан- та, а свойство Background окажется равным null? К счастью, все обойдется — окно просто закрашивает клиентскую область черным цветом и ожидает дальнейших распоряжений. А теперь мы перейдем к отображению реальных цветов в ListBox. Правильнее было бы выводить образец цвета вместе с названием, но сначала мы рассмотрим более простые альтернативы. До настоящего момента мы выводили в ListBox только текстовые строки. Однако вместо них можно заполнить список объекта- ми Shape — следующая программа показывает, как это делается. ListColorShapes.cs //------------------------------------------------ // ListColorShapes.cs (с) 2006 by Charles Petzold
279 UstBox using System; using System.Reflect!on: using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: using System.Windows.Shapes; namespace Petzold.Li stColorShapes ( class ListColorShapes : Window { [STAThread] public static void MainO { Application app = new Application!): app.Run(new ListColorShapesQ): } public ListColorShapes() { Title = "List Color Shapes": // Создание объекта ListBox как содержимого окна ListBox Istbox = new ListBoxO; Istbox.Width = 150: Istbox.Height = 150: Istbox.SelectionChanged += ListBoxOnSelectionChanged: Content = Istbox: // Заполнение ListBox объектами Ellipse Propertylnfo[] props = typeof(Brushes).GetPropertiesC): foreach (Propertyinfo prop in props) { Ellipse ellip = new EllipseO; ellip.Width = 100; ellip.Height = 25: ellip.Margin = new ThicknessdO, 5, 0, 5); ellip.Fill = prop.GetValue(null, null) as Brush: Istbox.Items.Add(ellip): } } void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs args) { ListBox Istbox = sender as ListBox: if (Istbox.Selectedlndex != -1) Background = (Istbox.Selectedltem as Shape).Fill: } } }
280 Глава 13. ListBox Конструктор окна создает объект Ellipse для каждого свойства класса Brushes и задает свойству Fill фигуры соответствующий объект Brush. Обработчик собы- тия достаточно прост: свойство Fill объекта Selectedltem просто задается свойству Background окна. В программе также можно воспользоваться механизмом привязки. Вместо того чтобы устанавливать обработчик события, вы указываете, что выделенное значение определяется свойством Fill варианта и оно должно быть привязано к свойству Background окна: Istbox.SelectedValuePath = "Fill"; 1stbox.SetBinding(ListBox.SeiectedValueProperty. "Background"); 1stbox.DataContext = this: А если в списке должно выводиться как название, так и образец цвета? Одно из возможных решений — заполнить ListBox элементами Label. Текст Label содер- жит название цвета, а фон определяет цвет. Конечно, нужно позаботиться о том, чтобы цвет текста не сливался с цветом фона, а объект Label имел достаточные внешние отступы, чтобы объект ListBox мог вывести цветовую пометку выделения. ListColoredLabels.cs // // ListColoredLabels.cs (с) 2006 by Charles Petzold // using System: using System.Reflection; using System.Windows; using System.Windows.Controls: using System.Windows.Input; using System.Windows.Media; namespace Petzold.Li stColoredLabels { class ListColoredLabels : Window { [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new ListColoredLabels()); } public ListColoredLabels() { Title = "List Colored Labels"; // Создание объекта ListBox как содержимого окна ListBox Istbox = new ListBoxO; Istbox.Height = 150;
281 ListBox Istbox.Width = 150; Istbox.SeiectlonChanged += LIstBoxOnSelectionChanged; Content = Istbox; // Заполнение списка элементами Label Propertylnfo[] props = typeof(Colors).GetPropert1es(); foreach (Propertyinfo prop in props) { Color clr = (Color)prop.GetValue(null, null); bool IsBlack = .222 * clr.R + .707 * clr.G + .071 * clr.В > 128; Label Ibl = new Label(); Ibl.Content = prop.Name; Ibl.Background = new SolldColorBrush(clr); Ibl.Foreground = isBlack ? Brushes.Black ; Brushes.White; Ibl.Width = 100; Ibl.Margin = new Th1ckness(15. 0. 0, 0); Ibl.Tag = clr; Istbox. Items.Adddbl); } } void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs args) { ListBox Istbox = sender as ListBox; Label Ibl = Istbox.Seiectedltern as Label; If (Ibl != null) { Color clr = (Color)lbl.Tag: Background = new SolidColorBrush(clr); } } } } Логическая величина isBlack вычисляется на основании яркости цвета, рав- ной стандартному средневзвешенному значению трех цветовых составляющих. Значение определяет, каким цветом должен выводиться текст на цветном фоне — черным или белым. Обработчик события мог бы задать фоновую кисть окна по значению свойст- ва Background объекта Label, но я решил воспользоваться свойством Тад — просто для разнообразия. Программа работает, однако внешний вид списка оставляет желать лучшего. Для отображения синей пометки текущего выделения объектам Label назначает- ся ширина 100, с отступами в 15 единиц у левого края и дополнительным ме- стом справа, обусловленным шириной самого объекта ListBox. Результат смотрит- ся не так элегантно, как хотелось бы.
282 Глава 13. ListBox Ранее я уже упоминал о классе ListBoxItem, производном от Contentcontrol. Ра- зумеется, вы не обязаны использовать этот класс в своих программах — приве- денные примеры доказывают это. Но если просто заменить Label в предыдущей программе на ListBoxItem, происходит нечто неприятное. ListBox использует свой- ство Background объекта ListBoxItem для пометки выделенного варианта, поэтому выделение остается практически незаметным. Но кто сказал, что мы должны использовать стандартную пометку? Можно создать альтернативный способ обозначения выделенного варианта. Следующая программа использует объекты ListBoxItem, обеспечивая при этом дополнитель- ную пометку выделенных вариантов. ListWithListBoxItems.cs //------------------------------------------------------ // L1stW1thL1stBoxItems.cs (с) 2006 by Charles Petzold //------------------------------------------------------ using System; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; class LIstWIthLIstBoxItems ; Window { [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new ListWithListBoxIterns()); } public ListWithLIstBoxItemsO Title = "List with ListBoxItem"; // Создание объекта ListBox как содержимого окна ListBox Istbox = new ListBoxO; Istbox.Height = 150; Istbox.Width = 150; Istbox.SelectionChanged += LIstBoxOnSelectlonChanged; Content = Istbox; // Заполнение списка объектами ListBoxItem Propertylnfo[] props = typeof(Colors).GetPropertlesO; foreach (Propertyinfo prop In props) { Color clr = (Color)prop.GetValue(null, null): bool isBlack = .222 * clr.R + .707 * clr.G + .071 * clr.В > 128:
283 ListBox ListBoxItem Item = new ListBoxItemO: item.Content = prop.Name; item.Background = new SolidColorBrush(clr); item.Foreground = IsBlack ? Brushes.Black : Brushes.White; item.HorizontalContentAlignment = Horizontal Alignment.Center; item.Padding = new Thickness(2); Istbox.Iterns.Add(itern); } void ListBoxOnSelectionChanged(object sender. SelectionChangedEventArgs args) { ListBox Istbox = sender as ListBox; ListBoxItem item; if (args.Removedltems.Count > 0) { item = args.RemovedItems[0] as ListBoxItem; String str = item.Content as String; item.Content = str.Substring^, str.Length - 4); item.FontWeight = Fontweights.Regular; } If (args.Addedltems.Count > 0) { item = args.AddedItems[O] as ListBoxItem; String str = item.Content as String; item.Content = "[ " + str + " ]"; item.FontWeight = FontWeights.Bold; } item = Istbox.Selectedltem as ListBoxItem; if (item != null) Background = item.Background; } } Обработчик события SelectionChanged не только задает фоновую кисть окна по значению свойства Background выделенного объекта ListBoxItem, но и изменяет текст выделенного варианта. Текст заключается в квадратные скобки и выделя- ется жирным начертанием. Конечно, при снятии выделения с варианта эти при- знаки необходимо отменить. Информация о том, какие варианты были выделе- ны или исключены из выделения, берется из свойств Addedltems и Removedltems объекта SelectionChangedEventArgs. Если жирное начертание и квадратные скобки не обеспечивают достаточного визуального выделения, можно увеличить размер шрифта. Включите следую- щую команду в секцию Addedltems; item.FontSize *= 2; Увеличение отменяется в секции Removedltems следующей командор!; item.FontSize = Istbox.FontSize;
284 Глава 13. ListBox Ширину ListBox также стоит увеличить с учетом увеличения размера шрифта. Класс ListBoxItem содержит два события Selected и Unselected, а также два ме- тода OnSelected и OnUnselected. Класс, производный от ListBoxItem, может переоп- ределить эти методы, чтобы обеспечить более естественную реализацию логики изменения текста и шрифта. Следующий класс, производный от ListBoxItem, переопределяет методы OnSelec- ted и OnUnselected. Впрочем, написан он для другой цели — продемонстрировать применение StackPanel для вывода как объекта Rectangle с образцом цвета, так и объекта TextBlock с его названием. ColorListBoxItem.cs //------------------------------------------------- // ColorL1stBoxItem.cs (с) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.LIstColorsElegantly class ColorLIstBoxItem : ListBoxItem { string str; Rectangle rect; TextBlock text; public ColorLIstBoxItemO { // Создание панели StackPanel для вывода Rectangle и TextBlock StackPanel stack = new StackPanel0; stack.Orientation = Orientation.Horizontal; Content = stack; // Создание объекта Rectangle для вывода образца цвета rect = new RectangleO; rect.Width = 16; rect.Height = 16; rect.Margin = new Th1ckness(2); rect.Stroke = SystemColors.WIndowTextBrush; stack.Children.Add(rect); // Создание объекта TextBlock для вывода названия цвета text = new TextBlockО; text.VerticalAlignment = VerticalAlignment.Center;
285 UstBox stack.Children.Add(text): } // Свойство Text становится свойством объекта TextBlock public string Text set { str = value; string strSpaced = str[0].ToStrlngO; for (Int 1 = 1; 1 < str.Length; 1++) strSpaced += (char.IsUpper(str[1]) ? " " : "") + str[1].ToStrlngO; text.Text = strSpaced; } get { return str; } } // Свойство Color становится свойством Brush объекта Rectangle public Color Color { set { rect.Fill = new SolidColorBrush(value); } get { SolidColorBrush brush = rect.Fill as SolidColorBrush; return brush == null ? Colors.Transparent : brush.Col or; } } // Выделенный вариант помечается жирным шрифтом protected override void OnSelected(RoutedEventArgs args) { base.OnSelected(args); text.FontWeight = FontWeights.Bold; } protected override void OnUnselected(RoutedEventArgs args) { base.OnUnselected(args); text.FontWeight = FontWeights.Regular; } // Реализация ToString для клавиатурного интерфейса public override string ToStrlngO { return str;
286 Глава 13. ListBox • Класс ColorListBoxItem создает в своем конструкторе объекты StackPanel (с го- ризонтальной ориентацией), Rectangle и TextBlock. Класс определяет свойство Text, связываемое со свойством Text объекта TextBlock, и свойство Color, которое свя- зывается с объектом Brush, используемым для закраски Rectangle. Для пометки текста выделенного варианта жирным шрифтом класс переопределяет методы OnSelected и OnUnselected. Класс ColorListBox, производный от ListBox, заполняет себя объектами типа ColorListBoxItem с использованием рефлексии. Он также определяет новое свой- ство SelectedColor, которое просто ссылается на SelectedValue. ColorListBox.cs //---------------------------------------------- // ColorListBox.cs (с) 2006 by Charles Petzold //---------------------------------------------- using System; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.LIstColorsElegantly { class ColorListBox : ListBox { public ColorListBoxQ { Propertylnfo[] props = typeof(Colors).GetPropertlesO; foreach (PropertyInfo prop In props) ColorListBoxItem Item = new ColorL1stBoxItem(); Item.Text = prop.Name; Item.Color = (Color)prop.GetValue(null. null); Iterns.Add(Item); } SelectedValuePath = "Color"; public Color SelectedColor { set { SelectedValue = value; } get { return (Color)SelectedValue; } } } } После определения классов ColorListBoxItem и ColorListBox код создания списка становится очень простым и элегантным. Столь же элегантно организован вы- вод цветных прямоугольников и текстовых строк в ListBox.
287 ListBox LlstColorsElegantly.cs U ListColorsElegantly.es (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls: using System.Windows.Input; using System.Windows.Media: namespace Petzold.ListColorsE1egantly ( public class ListColorsElegantly : Window { [STAThread] public static void MainO { Application app = new ApplicationO: app.Run(new ListColorsElegantly()); } public ListColorsElegantlyO { Title = "List Colors Elegantly"; ColorListBox Istbox = new ColorListBoxO; Istbox.Height = 150; Istbox.Width = 150; Istbox.SeiectionChanged += ListBoxOnSelectionChanged; Content = Istbox; // Инициализация SelectedColor Istbox.SelectedColor = SystemColors.WindowColor; } void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs args) { ColorListBox Istbox = sender as ColorListBox: Background = new SolidColorBrushdstbox.SelectedColor); } } И все же, нельзя ли найти в WPF еще более элегантное решение? Да, можно. Вспомните программу BuildButtonFactory из главы 11, в которой внешний вид кнопки переопределялся заданием свойства Template, определен- ного классом Control. Класс ListBox наследует это свойство, но ItemsControl опре- деляет еще два свойства, относящихся к шаблонам: свойство ItemTemplate (тина DataTemplate) определяет внешний вид вариантов, а также их привязки к источ- никам данных. Свойство ItemsPanel (типа ItemsPanelTemplate) позволяет задать Другую панель для отображения вариантов.
288 Глава 13. UstBox Программа ListColorsEvenElegantlier демонстрирует применение свойства ItemTemplate. Для ее работы также необходим класс NamedBrush, представленный ранее в этой главе. ListColorsEvenElegantlier.cs //----------------------------------------------------------- // ListColorsEvenElegantlier.cs (с) 2006 by Charles Petzold //----------------------------------------------------------- using Petzold.ListNamedBrushes: using System; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.Li stColorsEvenElegantlier { public class ListColorsEvenElegantlier : Window { [STAThread] public static void MainO Application app = new ApplicationO; app.Run(new ListColorsEvenElegantlierO); } public Li stColorsEvenElegantlier() Title = "List Colors Even Elegantlier"; // Создание шаблона DataTemplate для вариантов DataTemplate template = new DataTemplate(typeof(NamedBrush)); // Создание объекта FrameworkElementFactory для StackPanel FrameworkElementFactory factorystack = new FrameworkElementFactory(typeof(StackPanel)); factorystack.SetValue(StackPanel.Ori entati onProperty, Orientation.Horizontal); // Назначение объекта корнем визуального дерева DataTemplate tempi ate.VisualTree = factorystack; // Создание объекта FrameworkElementFactory для Rectangle FrameworkElementFactory factoryRectangle = new FrameworkElementFactory(typeof(Rectangle)); factoryRectangle.SetVa1ue(Rectangle.Wi dthProperty, 16.0); factoryRectangle.SetValue(Rectangle.Hei ghtProperty, 16.0);
289 ListBox factoryRectangle.SetValue(Rectangle.MarglnProperty, new Th1ckness(2)): factoryRectangle.SetValue(Rectangle.StrokeProperty, SystemColors.WIndowTextBrush); factoryRectangle.SetВ1nding(Rectangle.Fl 11 Property, new BindingCBrush")): // Присоединение к StackPanel factorystack.AppendChl1d(factoryRectangle): // Создание объекта FrameworkElementFactory для TextBlock FrameworkElementFactory factoryTextBlock = new FrameworkElementFactory(typeof(TextBlock)); factoryTextBlock.SetValue(TextBlock.Vertical Al 1gnmentProperty. Vert1ca1 Al 1gnment.Center); factoryTextBlock.SetValue(TextBlock.TextProperty, new BindingCName")); // Присоединение к StackPanel factorystack.AppendChl1d(factoryTextBlock): // Создание объекта ListBox как содержимого окна ListBox Istbox = new LIstBoxO; Istbox.Width = 150; Istbox.Height = 150; Content = Istbox: // Свойству ItemTemplate задается ранее созданный шаблон Istbox.ItemTemplate = template; // Свойству ItemsSource задается массив объектов NamedBrush Istbox.ItemsSource = NamedBrush.All; // SelectedValue привязывается к свойству Background окна Istbox.SelectedValuePath = "Brush": 1stbox.SetBlnding(LIstBox.SeiectedValueProperty. "Background"): 1stbox.DataContext = this; } } } Конструктор окна начинается с создания объекта типа DataTemplate (в конце конструктора этот объект задается свойству ItemTemplate объекта ListBox). Обра- тите внимание: конструктору DataTemplate передается тип класса NamedBrush. Это означает, что ListBox будет заполняться объектами типа NamedBrush. Затем программа переходит к построению визуального дерева для отображе- ния вариантов. В корне дерева располагается панель StackPanel. Объект Framework- ElementFactory для StackPanel задается свойству VisualTree объекта DataTemplate.
290 Глава 13. ListBox Затем создаются еще два объекта FrameworkElementFactory (для Rectangle и Text- Block), которые включаются в коллекцию дочерних объектов объекта Framework- ElementFactory для StackPanel. Свойство Fill элемента Rectangle связывается со свой- ством Brush объекта NamedBrush, а свойство Text элемента TextBlock — со свойством Name объекта NamedBrush. Теперь можно переходить к созданию ListBox. Завершение программы нам уже знакомо, если не считать задания только что созданного объекта DataTemplate свойству ItemTemplate объекта ListBox. Отныне ListBox будет отображать объекты NamedBrush в визуальном дереве этого шаблона. Конструктор завершается заполнением списка, для чего используется свой- ство ItemsSource и статическое свойство АП класса NamedBrush. Свойство Selected- ValuePath задает свойство Brush объекта NamedBrush и связывается со свойством Background окна. Как было сказано в начале главы, элемент ListBox чаще всего используется для выбора одного варианта из многих. В главе 11 элемент ColorGrid выводил 40 элементов ColorCell на панели UniformGrid и предоставлял обширный интер- фейс клавиатуры и мыши для выбора цвета из палитры. Замена ColorGrid эле- ментом ListBox позволила бы избежать дублирования кода клавиатуры и мыши, уже присутствующего в ListBox. Все, что для этого потребуется — заставить ListBox использовать UniformGrid вместо StackPanel для вывода содержимого. В сле- дующем классе эта задача решается всего тремя командами. Класс ColorGridBox, производный от ListBox, обладает практически полной функциональностью эле- мента ColorGrid из главы 11 (отсутствуют лишь некоторые второстепенные воз- можности). ColorGridBox.cs //--------------------------------------------- // ColorGridBox.cs (с) 2006 by Charles Petzold //--------------------------------------------- using System; using System.Windows: using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media: using System.Windows.Shapes; namespace Petzold.Sei ectColorFromGri d class ColorGridBox : ListBox { // Отображаемые цвета stringf] strColors = { "Black", "Brown", "DarkGreen", "MidnightBlue",
291 ListBox "Navy", "DarkBlue". "Indigo". "DimGray". "DarkRed", "OrangeRed", "Olive". "Green". "Teal", "Blue". "SlateGray", "Gray". "Red". "Orange", "YellowGreen", "SeaGreen", "Aqua". "LightBlue", "Violet". "DarkGray", "Pink". "Gold". "Yellow". "Lime". "Turquoise", "SkyBlue", "Plum". "LightGray", "LightPink", "Tan". "LightYellow", "LightGreen", "LightCyan", "LightSkyBlue", "Lavender". "White" }: public ColorGridBoxO { // Определение шаблона ItemsPanel FrameworkElementFactory factoryllnlgricl = new FrameworkElementFactory(typeof(UniformGrid)): factoryllnlgrid.SetValue(UniformGrid.ColumnsProperty, 8); ItemsPanel = new ItemsPanelTemplate(factoryUnigrid); // Заполнение списка foreach (string strColor in strColors) { // Создание объекта Rectangle и его включение в ListBox Rectangle rect = new RectangleO; rect.Width = 12; rect.Height = 12: rect.Margin = new Thickness(4); rect.Fill = (Brush) typeof(Brushes).GetProperty(strColor).GetValue(null, null): Iterns.Add(rect): // Создание объекта ToolTip для Rectangle ToolTip tip = new ToolTipO; tip.Content = strColor; rect.ToolTip = tip; } // В качестве SelectedValue выбирается // свойство Fill объекта Rectangle SelectedValuePath = "Fill"; } } } В начале конструктора создается объект FrameworkElementFactory для пане- ли UniformGrid, а свойство Columns задается равным 8. Далее объект передается конструктору ItemsPanelTemplate, а объект ItemsPanelTemplate задается свойству ItemsPanel объекта ListBox. Вот и все, что потребуется для того, чтобы объект UstBox использовал другую панель вместо StackPanel.
292 Глава 13. ListBox Большая часть кода конструктора заполняет ListBox объектами Rectangle, при- чем каждый объект снабжается подсказкой ToolTip с названием цвета. Напосле- док конструктор заносит в свойство SelectedValuePath класса ListBox ссылку на свойство Fill объекта Rectangle. Программа SelectColorFromGrid очень похожа на программу SelectColor, ис- пользовавшуюся в главе 11 для тестирования нового элемента. Основное раз- личие заключается в том, что новая программа связывает свойство SelectedValue элемента ColorGridBox со свойством Background окна. SelectColorFromGrid.cs // // Sei ectCol orFromGrid.es (с) 2006 by Charles Petzold // using System; using System.Windows: using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.Sei ectColorFromGrld { public class SelectColorFromGrid : Window { [STAThread] public static void MalnO { Application app = new ApplicationO; app.Run(new Sei ectColorFromGri d()): } public SelectColorFromGr1d() Title = "Select Color from Grid"; SizeToContent = SizeToContent.WldthAndHelght; // Создание объекта StackPanel как содержимого окна StackPanel stack = new StackPanel0: stack.Orientation = Orientation.Horizontal; Content = stack; // Фиктивная кнопка для проверки передачи фокуса Button btn = new ButtonO; btn.Content = "Do-nothing button\nto test tabbing"; btn.Margin = new Th1ckness(24): btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; stack.Children.Add(btn);
293 ListBox // Создание элемента ColorGridBox ColorGridBox clrgrid = new ColorGridBox(); clrgrid.Margin = new Thickness(24); clrgrid.HorizontalAlignment = HorizontalAlignment.Center: clrgrid.VerticalAlignment = Vertical Alignment.Center; stack.Chi 1dren.Add(clrgrid); // Привязка свойства Background окна // к выделенному значению ColorGridBox clrgrid.SetBi ndi ng(ColorGridBox.SeiectedValueProperty. "Background"): clrgrid.DataContext = this: // Создание другой фиктивной кнопки btn = new ButtonO: btn.Content = "Do-nothing button\nto test tabbing": btn.Margin = new Thickness(24); btn.Horizontal Alignment = HorizontalAlignment.Center: btn.VerticalAlignment = VerticalAlignment.Center; stack.Children.Add(btn); } } } В частности, проверьте работу клавиатурного интерфейса ColorGridBox. Клави- ши со стрелками перемещают выделение, а клавиши PageUp и PageDown осуще- ствляют переход в начало и конец столбца. Клавиша Ноте переходит к первому элементу, а клавиша End — к последнему. Я убежден, что список ListBox реализует обобщенную логику клавиатурной навигации, основанную на относительной позиции вариантов при отображении на экране. Этот факт становится более очевидным в следующей программе, где в ListBox подставляется панель RadialPanel из главы 12, используемая для отобра- жения 141 цвета из класса Brushes в объектах Rectangle. Класс, как и ColorGridBox, является производным от ListBox, а в начале его конструктора свойству Items- Panel объекта ListBox задается объект ItemsPanelTemplate, основанный на объекте FrameworkElementFactory альтернативной панели. ColorWheel.cs Ц // ColorWheel.cs (с) 2006 by Charles Petzold И-------------------------------------------- using Petzold.Ci rcleTheButtons; using System: using System.Reflection: using System.Windows: using System.Windows.Controls: using System.Windows.Data: using System.Windows.Input;
294 Глава 13. ListBox using System.Windows.Media: using System.Windows.Shapes: namespace Petzold.Sei ectColorFromWheel { class ColorWheel : ListBox { public ColorWheel() { // Определение шаблона ItemsPanel FrameworkElementFactory factoryRadial Panel = new FrameworkElementFactory(typeof(RadialPanel)): ItemsPanel = new ItemsPanelTemplate(factoryRadialPanel); // Создание объекта DataTemplate для вариантов DataTemplate template = new DataTemplate(typeof(Brush)): ItemTemplate = template: // Создание объекта FrameworkElementFactory на базе Rectangle FrameworkElementFactory el Rectangle = new FrameworkElementFactory(typeof(Rectangle)): el Rectangle.SetVa1ue(Rectangle.WidthProperty, 4.0): el Rectangle.SetValue(Rectangle.HeightProperty, 12.0): elRectangle.SetValue(Rectangle.Margi nProperty. new Thicknessd, 8, 1, 8)): elRectangle.SetBi nding(Rectangle.Fi11 Property, new Binding("")); // Объект задается в качестве визуального дерева template.VisualTree = el Rectangle: // Заполнение списка ListBox PropertyInfoE] props = typeof(Brushes).GetPropertiesO: foreach (Propertyinfo prop in props) Items.Add((Brush)prop.GetValue(null. null)): } } } После задания свойства ItemsPanel конструктор переходит к определению объ- екта DataTemplate, который задается свойству ItemTemplate объекта ListBox. Объект DataTemplate указывает, что содержащиеся в списке варианты относятся к типу Brush. Визуальное дерево состоит из одного элемента типа Rectangle, а свойство Fill привязывается к самому варианту (обозначается пустым строковым аргумен- том в конструкторе Binding). Работа конструктора завершается заполнением ListBox всеми статическими свойствами класса Brushes. Задавать свойство SelectedValuePath объекта ListBox не обязательно, потому что варианты ListBox представляют собой объекты Brush. Без пользовательского объекта ItemTemplate они бы отображались в виде шест- надцатеричных кодов RGB.
295 UstBox Программа SelectColor From Wheel, почти идентичная SelectColorFromGrid, позволяет поэкспериментировать с элементом ColorWheel. SelectColorFromWheel.es и // SelectColorFromWheel.cs (с) 2006 by Charles Petzold // using System; using System.Windows: using System.Windows.Controls; using System.Windows.Input: using System.Windows.Media: namespace Petzol d.Sei ectColorFromWheel { public class Sei ectColorFromWheel : Window { [STAThread] public static void MainO { Application app = new Application); app.Run(new Sei ectColorFromWheel()); } public Sei ectColorFromWheel() { Title = "Select Color from Wheel"; SizeToContent = SizeToContent.WidthAndHeight: // Создание объекта StackPanel как содержимого окна StackPanel stack = new StackPanel0: stack.Orientation = Orientation.Horizontal: Content = stack: // Создание фиктивной кнопки для проверки передачи фокуса Button btn = new ButtonO; btn.Content = "Do-nothing button\nto test tabbing": btn.Margin = new Th1ckness(24): btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center: stack.Children.Add(btn); // Создание элемента ColorWheel ColorWheel clrwheel = new ColorWheel(); clrwheel.Margin = new Th1ckness(24); clrwheel.HorizontalAlignment = HorizontalAlignment.Center: clrwheel.VerticalAlignment = VerticalAlignment.Center; stack.ChiIdren.Add(clrwheel):
296 Глава 13. ListBox // Привязка свойства Background окна к выделенному цвету clrwheel.SetBinding(ColorWheel.SelectedValueProperty, "Background"): clrwheel.DataContext = this: // Создание другой фиктивной кнопки button btn = new ButtonO: btn.Content = "Do-nothing button\nto test tabbing": btn.Margin = new Thickness(24); btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center: stack.Children.Add(btn); } } } Клавиатурный интерфейс выглядит довольно необычно. Клавиши t и пе- ремещают выделение только в границах левой или правой половины круга; вы- деление не может переместиться из одной половины в другую. Аналогично, кла- виши <— и -> работают только в верхней или нижней половине круга. Чтобы переместить выделение по всем элементам (например, по часовой стрелке), вам придется переключиться с клавиши -> на клавишу в правой верхней четверти круга, затем с на <- в правой нижней четверти и т. д. Модификация ListBox открывает чрезвычайно широкие возможности. Каждый раз, когда пользователю нужно предоставить возможность выбора одного вариан- та из многих, подумайте об использовании ListBox. Сначала взгляните на ListBox с абстрактной точки зрения, а затем подумайте, как нужно изменить элемент, чтобы наделить его желаемым внешним видом и поведением.
14 Иерархия меню Центральное место в пользовательском интерфейсе приложений Windows тра- диционно занимает меню. Обычно оно располагается в верхней части окна при- ложения, под полосой заголовка, и распространяется на полную ширину окна. В наиболее распространенной форме меню представляет собой горизонтальный список текстовых команд. Щелчок на команде меню верхнего уровня обычно открывает список команд следующего уровня, называемый раскрывающимся ме- ню или подменю. Каждое подменю состоит из текстовых команд, которые вы- полняют некоторое действие или открывают следующее подменю. Короче говоря, меню имеет иерархическую структуру. Каждая команда меню представлена объектом типа Menuitem. Само меню представлено объектом типа Menu. Следующая иерархия классов дает представление о том, какое место зани- мают эти два элемента в иерархии WPF: Control Contentcontrol HeaderedContentControl ItemsControl HeaderedltemsControl Эти четыре класса, производных от Control, включают многие знакомые эле- менты: О Элементы, производные от Contentcontrol, характеризуются наличием свой- ства Content. К их числу принадлежат кнопки, надписи, экранные подсказки (ToolTip) и сами окна. О В классе HeaderedContentControl, производном от Contentcontrol, добавляется свойство Header. К этой категории относятся групповые поля (GroupBox). О Класс ItemsControl определяет свойство Items, которое представляет собой коллекцию других объектов. К этой категории относятся списки и комбини- рованные поля. О В классе HeaderedltemsControls к свойствам, унаследованным от ItemsControl, до- бавляется свойство Header. Одним из элементов этой категории является меню. Свойство Header объекта Menuitem обеспечивает визуальное представление команды меню — как правило, это короткая текстовая строка, которая может
298 Глава 14. Иерархия меню сопровождаться необязательным растровым изображением. Каждая команда меню также может содержать коллекцию команд, отображаемых в подменю. Эти команды хранятся в свойстве Items. У команд меню, предназначенных для прямого выполнения операций, коллекция Items пуста. Например, меню верхнего уровня обычно начинается с команды File. Эта ко- манда представлена объектом Menuitem. Свойство Header содержит текстовую строку «File», а коллекция Items содержит объекты Menuitem для операций New, Open, Save и т. д. Единственной частью иерархии меню, которая не подчиняется описанной схеме, является меню верхнего уровня. Бесспорно, оно представляет собой кол- лекцию команд (File, Edit, View, Help и т. д.), но заголовок с этой коллекцией не связывается. По этой причине меню верхнего уровня представляется объектом типа Menu, производным от ItemsControls. Частичная иерархия классов меню вы- глядит так: Control ItemsControl HeaderltemsControl Menuitem MenuBase (абстрактный) ContextMenu Menu Separator Элемент Separator просто рисует горизонтальную или вертикальную раздели- тельную линию (в зависимости от контекста). Такие линии часто используются в подменю для деления команд на несколько функциональных категорий. На самом деле команды меню могут представляться объектами практиче- ски любого типа, но на практике обычно используется тип Menuitem, потому что в нем определены некоторые свойства и события, часто ассоциируемые с коман- дами меню. Класс Menuitem, как и ButtonBase, определяет событие Click и свойст- во Command. Следовательно, программа может работать со многими командами меню так, как обычно работает с кнопками. Класс Menuitem также определяет свойство с именем Icon. Оно позволяет раз- местить небольшой рисунок в стандартной позиции меню. Кстати говоря, свой- ство Icon относится к типу Object, поэтому ему можно задавать элементы из биб- лиотеки Shapes (см. далее). Команды меню могут снабжаться визуальной пометкой — либо для обозна- чения режимов с двумя состояниями (вкл/выкл), либо для обозначения одной команды, выбранной из группы взаимоисключающих команд. Логическое свой- ство IsChecked устанавливает или снимает пометку, а свойство IsCheckable авто- матизирует переключение пометок. Событие Checked инициируется при переходе свойства IsChecked из состоя- ния false в true, а свойство Unchecked является признаком обратного перехода, из true в false. Иногда программе требуется заблокировать (сделать недоступными) некоторые команды подменю. Например, если программа в настоящее время не содер-
Иерархия меню 299 жИТ несохраненных изменений, команда Save меню File, как правило, недоступна. Обычно команды бывает удобно блокировать при отображении подменю. Класс Menuitem определяет событие SubmenuOpened, упрощающее решение этой задачи. Построение меню обычно начинается на верхнем уровне иерархии. Сначала создается объект типа Menu: Menu menu = new MenuO; Как правило, меню верхнего уровня начинается с команды File: Menuitem itemFile = new MenuItemO; itemFile.Header = "-File": Как и в других элементах, символ подчеркивания упрощает активизацию ко- манды с клавиатуры. Когда пользователь нажимает клавишу Alt, буква F в слове File выделяется подчеркиванием, а при нажатии клавиши F открывается подме- ню File. В меню верхнего уровня, а также в каждом подменю подчеркнутые буквы должны быть уникальны. Чтобы включить команду File в меню верхнего уровня, добавьте ее в коллек- цию Items объекта Menu: menu.Items.Add(itemFile); Меню File часто начинается с команды New: Menuitem itemNew = new MenuItemO; itemNew.Header = "_New"; itemNew.Click += NewOnClick: itemFile.Items.Add(itemNew); Команда New предназначена для выполнения операции, поэтому для нее на- значается обработчик события Click. Объект включается в коллекцию объекта команды File: itemFile.Items.Add(itemNew); И так далее. Следующая программа выводит в клиентской области короткий текст и строит меню. В меню File реализована только одна команда, но меню Window содержит четыре команды-переключателя, позволяющие изменять свойства окна. PeruseTheMenu.cs //--------------------------------------------- И PeruseTheMenu.cs (с) 2006 by Charles Petzold //--------------------------------------------- using System: using System.Windows; using Systern.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.PeruseTheMenu
300 Глава 14. Иерархия меню { public class PeruseTheMenu : Window { [STAThread] public static void MainQ { Application app = new Application); app.Run(new PeruseTheMenuO); } public PeruseTheMenuO { Title = "Peruse the Menu"; // Создание объекта DockPanel DockPanel dock = new DockPanel(); Content = dock; // Создание меню, пристыкованного у верхнего края окна Menu menu = new Menu О; dock.Children.Add(menu); DockPanel.SetDock(menu, Dock.Top); // Создание объекта TextBlock, заполняющего оставшуюся площадь TextBlock text = new TextBlock О; text.Text = Title; text.FontSize =32; // 24 пункта text.TextAlignment = TextAlignment.Center; dock.Children.Add(text); // Создание меню File Menuitem itemFile = new MenuItemO; itemFile.Header = "_File"; menu.Items.Add(itemFile); Menuitem itemNew = new MenuItemO; itemNew.Header = "_New"; itemNew.Click += UnimplementedOnClick; itemFile.Items.Add(itemNew); Menuitem itemOpen = new MenuItemO; itemOpen.Header = "_Open"; itemOpen.Click += UnimplementedOnClick; i temF i1e.Iterns.Add(i temOpen); Menuitem itemSave = new MenuItemO; itemSave.Header = "_Save"; itemSave.Click += UnimplementedOnClick; itemFile.Items.Add(itemSave); itemFile.Items.Add(new SeparatorO); Menuitem itemExit = new MenuItemO;
Иерархия меню 301 itemExit.Header = "E_xit"; itemExit.Click += ExitOnClick: itemFile.Iterns.Add(itemExit); // Создание меню Window Menuitem itemWindow = new MenuItemO; itemWindow.Header = "_Window"; menu.Items.Add(itemWindow); Menuitem itemTaskbar = new MenuItemO; itemTaskbar.Header = "_Show in Taskbar"; itemTaskbar.IsCheckable = true; itemTaskbar.IsChecked = ShowInTaskbar; itemTaskbar.Click += TaskbarOnClick; itemWindow.Items.Add(itemTaskbar); Menuitem itemSize = new MenuItemO; itemSize.Header = "Size to _Content"; itemSize.IsCheckable = true; itemSize.IsChecked = SizeToContent == SizeToContent.WidthAndHeight; itemSize.Checked += SizeOnCheck: itemSize.Unchecked += SizeOnCheck; itemWindow.Items.Add(itemSize); Menuitem itemResize = new MenuItemO; itemResize.Header = "_Resizable"; itemResize.IsCheckable = true; itemResize.IsChecked = ResizeMode == ResizeMode.CanResize; itemResize.Click += ResizeOnClick; i temWi ndow.Iterns.Add(i temRes i ze); Menuitem itemTopmost = new MenuItemO; itemTopmost.Header = "_Topmost"; itemTopmost.IsCheckable = true; itemTopmost.IsChecked = Topmost; itemTopmost.Checked += TopmostOnCheck; itemTopmost.Unchecked += TopmostOnCheck; itemWindow.Iterns.Add(itemTopmost); } void UnimplementedOnClick(object sender. RoutedEventArgs args) Menuitem item = sender as Menuitem; string stritem = item.Header.ToStringO.Replace(, ""); MessageBox.ShowC’The " + str Item + " option has not yet been implemented". Title); } void ExitOnClick(object sender. RoutedEventArgs args) { CloseO: 1 / void TaskbarOnClick(object sender. RoutedEventArgs args)
302 Глава 14. Иерархия меню Menuitem item = sender as Menuitem: ShowInTaskbar = item.IsChecked; void SizeOnCheck(object sender, RoutedEventArgs args) { Menuitem item = sender as Menuitem: SizeToContent = item.IsChecked ? SizeToContent.WidthAndHeight : SizeToContent.Manual: void ResizeOnClick(object sender, RoutedEventArgs args) { Menuitem item = sender as Menuitem: ResizeMode = item.IsChecked ? ResizeMode.CanResize : ResizeMode.NoResize; } void TopmostOnCheck(object sender, RoutedEventArgs args) { Menuitem item = sender as Menuitem: Topmost = item.IsChecked: } Код конструктора начинается с создания объекта DockPanel. В большинстве приложений меню пристыковывается у верхнего края клиентской области; если вы не хотите сбить с толку своих пользователей, вероятно, вам стоит разместить его там же. DockPanel — стандартная базовая панель для окон с меню, панелями инструментов и строками состояния. Того же эффекта можно добиться с пане- лями StackPanel и Grid, но стандартной является именно DockPanel. Метод UnimplementedOnClick обрабатывает события Click для команд New, Open и Save. Обработчик Click команды Exit вызывает Close для объекта Window, чтобы завершить работу программы. Объект Separator отделяет команду Exit от других команд, как это принято. У всех четырех команд меню Window свойство IsCheckable задано равным true; так обеспечивается автоматическое переключение пометки. Свойство IsChecked указывает, отображается пометка в настоящий момент или нет. Для первой и третьей команды программа устанавливает обработчик события Click, а для второй и четвертой команд тот же обработчик устанавливается для событий Checked и Unchecked. Выбор конкретного способа особой роли не играет. Теорети- чески установка отдельных обработчиков Checked и Unchecked позволяет выпол- нять действия без проверки свойства IsChecked. В этой программе обработчики Click, Checked и Unchecked просто используют свойство IsChecked для задания не- которых свойств окна. Попробуйте закомментировать следующие две строки кода: itemTaskbar.IsChecked = ShowInTaskbar: itemTaskbar.Click += TaskbarOnClick:
Иерархия меню 303 и заменить их следующим фрагментом: jtemTaskbar.SetBinding(MenuItem.IsCheckedProperty, "ShowInTaskbar"); itemTaskbar.DataContext = this: фактически мы сообщаем Menuitem, что свойство IsChecked всегда должно иметь такое же значение, как и свойство ShowInTaskbar объекта this (окна). При- вязку данных можно настроить и для команды меню Topmost, но для двух дру- гих меню этот способ не сработает, потому что свойства не относятся к логиче- скому типу. Если вы хотите, чтобы команда меню была помечена, не задавая свойству IsCheckable истинное значение, вам придется обрабатывать события Click и вруч- ную снимать и устанавливать пометку с использованием свойства IsChecked. В частности, этот способ применяется для отображения в меню групп команд взаимоисключающим состоянием, когда установка пометки любой команды приводит к снятию предыдущей пометки (по аналогии с группой кнопок-пе- реключателей). При работе с группами взаимоисключающих команд свойству IsCheckable остается значение по умолчанию false, чтобы предотвратить автома- тическое переключение пометки; кроме того, при исходном создании команд обычно устанавливается свойство IsChecked одной из них. Вся логика установки и снятия пометки реализуется в обработчиках события Click. Часто бывает проще использовать один обработчик Click для всех команд взаимоисключающей группы. Вероятно, для отслеживания текущей выделенной команды во взаимоисклю- чающих группах стоит ввести поле типа Menuitem: Menuitem itemChecked; Поле инициализируется в конструкторе окна. Если вся группа взаимоисклю- чающих команд обслуживается одним обработчиком Click, этот обработчик на- чинается со снятия текущей пометки: itemChecked.IsChecked = false: Затем обработчик сохраняет команду, на которой был сделан щелчок, в каче- стве нового значения itemChecked и устанавливает ее пометку: itemChecked = args.Source as Menuitem: itemChecked.IsChecked = true: Далее обработчик делает то, что необходимо сделать для этих команд. Сле- дующая программа, напоминающая программу TuneTheRadio из главы 5, позво- ляет изменить свойство Windowstyle окна. CheckTheWindowStyle.cs //------------------------------------------------- // CheckTheWindowStyle.cs (с) 2006 by Charles Petzold И-------------------------------------------------- using System: using System.Windows; using System.Windows.Controls;
304 Глава 14. Иерархия меню using System.Windows.Input; using System.Windows.Media; namespace Petzold.CheckTheWi ndowSty]e { public class CheckTheWindowStyle : Window { MenuItern itemChecked; [STAThread] public static void MainO Application app = new ApplicationO; app.Run(new CheckTheWindowStyleO); public CheckTheWindowSty1e() { Title = "Check the Window Style"; // Создание объекта DockPanel DockPanel dock = new DockPanel 0; Content = dock; // Создание меню, пристыкованного у верхнего края окна Menu menu = new MenuO; dock.Children.Add(menu); DockPanel.SetDock(menu, Dock.Top); // Создание объекта TextBlock, заполняющего оставшуюся площадь TextBlock text = new TextBlockO; text.Text = Title; text.FontSize = 32; text.TextAlignment = TextAlignment.Center; dock.Children.Add(text); // Создание объектов Menuitem для изменения WindowStyle Menu I tern itemStyle = new MenuItemO; itemStyle.Header = "_Style"; menu.Iterns.Add(itemStyle); itemStyle.Iterns.Add( CreateMenuItem("_No border or caption", WindowStyle.None)); itemStyle.Iterns.Add( CreateMenuItem("_Single-border window", Wi ndowStyle.Si ngleBorderWi ndow)); itemStyle.Items.Add( CreateMenuItem("3_D-border window", Wi ndowStyle.ThreeDBorderWi ndow)); itemStyle.Iterns.Add( CreateMenuItem("_Tool window",
Иерархия меню 305 Wi ndowSty1е.ToolWi ndow)); Menuitem CreateMenuItem(string str, WindowStyle style) { Menuitem item = new MenuItemO: item.Header = str: item.Tag = style: item.IsChecked = (style == WindowStyle): item.Click += StyleOnClick; if (item.IsChecked) itemChecked = item; return item: } void StyleOnClickCobject sender, RoutedEventArgs args) { itemChecked.IsChecked = false: itemChecked = args.Source as Menuitem: itemChecked.IsChecked = true: WindowStyle = (WindowStyle)itemChecked.Tag: } } ) Четыре команды меню Style похожи друг на друга и используют общий об- работчик события Click, поэтому программа определяет вспомогательный метод CreateMenuItem, предназначенный специально для создания этих команд. Каждая команда содержит текстовую строку с описанием значения перечисления Window- Style. Само значение передается в свойстве Тад объекта Menuitem, предназначен- ном как раз для подобных целей. Обработчик события Click снимает пометку с команды itemChecked, сохраняет в itemChecked объект команды, на которой был сделан щелчок, а напоследок за- дает свойству WindowStyle значение свойства Тад этой команды. В принципе программе CheckTheWindowStyle не обязательно поддерживать поле itemChecked — оно предназначено исключительно для удобства. Программа всегда может определить помеченную команду перебором коллекции Items ко- манды Style или даже прямой проверкой свойства WindowStyle окна. Более того, не обязательно даже устанавливать и снимать пометку в обработ- чике события Click. Вместо этого можно подготовить подменю во время обра- ботки события SubmenuOpened и пометить правильную команду на этой стадии. Следующая программа демонстрирует альтернативный подход к установлению и снятию пометки команд. Команды меню в этом приложении изменяют основ- ную и фоновую кисти элемента TextBlock. CheckTheColor.cs //--------------------------------------------- // CheckTheColor.cs (с) 2006 by Charles Petzold //---------------------------------------------
306 Глава 14. Иерархия меню using System; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.CheckTheColor public class CheckTheColor : Window TextBlock text; [STAThread] public static void MainO Application app = new ApplicationO; app.Run(new CheckTheColorO); } public CheckTheColorO Title = "Check the Color": // Создание объекта DockPanel DockPanel dock = new DockPanel 0; Content = dock; // Создание меню, пристыкованного у верхнего края окна Menu menu = new MenuO; dock.Children.Add(menu); DockPanel.SetDock(menu, Dock.Top); // Создание объекта TextBlock. заполняющего оставшуюся площадь text = new TextBlock(); text.Text = Title; text.TextAlignment = TextAlignment.Center; text.FontSize = 32; text.Background = SystemColors.WindowBrush; text.Foreground = SystemColors.WindowTextBrush: dock.Children.Add(text); // Создание команд меню Menuitem itemColor = new MenuItemO; itemColor.Header = "_Color"; menu.Items.Add(itemColor); Menuitem itemForeground = new MenuItemO; itemForeground.Header = "-Foreground"; itemForeground.SubmenuOpened += ForegroundOnOpened; itemColor.Items.Add(itemForeground); FillWithColors(itemForeground, ForegroundOnClick); Menuitem itemBackground = new MenuItemO;
Иерархия меню 307 itemBackground.Header = "-Background"; itemBackground.SubmenuOpened += BackgroundOnOpened: itemColor.Items.Add(itemBackground); Fi11WithColors(itemBackground, BackgroundOnClick); } void FillWithColors(MenuItem itemParent, RoutedEventHandler handler) foreach (Propertyinfo prop in typeof(Colors) .GetPropertiesO) { Color clr = (Color)prop.GetValue(null, null): int iCount = 0; iCount += clr.R == 0 || clr.R == 255 ? 1 : 0; iCount += clr.G == 0 || clr.G == 255 ? 1 : 0; iCount += clr.В == 0 || clr.В == 255 ? 1 ; 0; if (clr.A == 255 && iCount > 1) Menuitem item = new MenuItemO; item.Header = "_" + prop.Name; item.Tag = clr; item.Click += handler; itemParent.Items.Add(item); } } void Foreground0n0pened(object sender, RoutedEventArgs args) Menuitem itemParent = sender as Menuitem; foreach (Menuitem item in itemParent.Items) item.IsChecked = ((text.Foreground as SolidColorBrush).Color == (Color)item.Tag); } void Background0n0pened(object sender, RoutedEventArgs args) Menuitem itemParent = sender as Menuitem; foreach (Menuitem item in itemParent.Items) item.IsChecked = ((text.Background as SolidColorBrush).Color == (Color)item.Tag); } void ForegroundOnClick(object sender. RoutedEventArgs args) { Menuitem item = sender as Menuitem; Color clr = (Color)item.Tag; text.Foreground = new SolidColorBrush(clr); } void BackgroundOnClick(object sender, RoutedEventArgs args)
308 Глава 14. Иерархия меню { Menuitem item = sender as Menuitem; Color clr = (Color)item.Tag; text.Background = new SolidColorBrush(clr); } } } Программа создает команду меню верхнего уровня Color и подменю с двумя командами, Foreground и Background. Для каждой из этих команд метод FillWithColors добавляет команды, соответствующие цветам, во вложенное подменю. Меню ог- раничивается цветами, у которых по крайней мере две из трех составляющих RGB равны 0 или 255. Что касается меню Foreground, метод ForegroundOnOpened обрабатывает событие SubmenuOpened, а метод ForegroundOnClick — событие Click каждого из цветов под- меню Foreground (меню Background работает аналогично). Обработчик Foreground- OnOpened перебирает содержимое коллекции Items и задает свойство IsChecked равным true, если значение соответствует текущему цвету TextBlock, или false в противном случае. Методу ForegroundOnClick не нужно возиться с установлени- ем и снятием пометки — достаточно создать новую кисть для TextBlock. Можно ли вывести в меню образцы цветов? Конечно, можно. Класс Menuitem содержит свойство Icon для отображения маленьких значков (или других изобра- жений) в левой части команды меню. Включите следующий фрагмент в блок if метода FillWithColors, в любую точку после создания Menuitem (в программу уже включена директива using для пространства имен System.Windows.Shapes): Rectangle rect = new RectangleO; rect.Fill = new SolidColorBrush(clr): rect.Width =2* (rect.Height = 12); item.Icon = rect; Вероятно, вы еще помните элемент ColorGrid из главы И, а также похожий на него (но более простой) элемент ColorGridBox из главы 13. Эти элементы могут использоваться в качестве команд меню; следующая программа показывает, как это делается. Для ее работы необходима ссылка на файл ColorGridBox.cs. Обрати- те внимание; в программу включена директива using для указанного проекта. SelectColorFromMenuGrid.cs // // Sei ectCol orFromMenuGrid.cs (с) 2006 by Charles Petzold // using Petzold.Sei ectColorFromGri d; using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media;
Иерархия меню 309 namespace Petzold.Sei ectColorFromMenuGr1d public class Sei ectColorFromMenuGrid : Window { [STAThread] public static void MainO { Application app = new Application); app.Run(new SelectColorFromMenuGridO): } public SelectColorFromMenuGrid() { Title = "Select Color from Menu Grid"; // Создание объекта DockPanel DockPanel dock = new DockPanel(); Content = dock: // Создание меню, пристыкованного у верхнего края окна Menu menu = new Menu О; dock.Children.Add(menu): DockPanel.SetDock(menu, Dock.Top); // Создание объекта TextBlock, заполняющего оставшуюся площадь TextBlock text = new TextBlock О; text.Text = Title: text.FontSize = 32; text.TextAlignment = TextAlignment.Center; dock.Chi 1dren.Add(text): // Включение команд в меню Menuitem itemColor = new MenuItemO; itemColor.Header = "_Color"; menu.Items.Add(itemColor); Menuitem itemForeground = new MenuItemO; itemForeground.Header = "-Foreground"; itemColor.Items.Add(itemForeground): // Создание объекта ColorGridBox и его привязка // к свойству Foreground окна ColorGridBox clrbox = new ColorGridBoxO; clrbox.SetBinding(ColorGridBox.SeiectedValueProperty, "Foreground"): clrbox.DataContext = this; itemForeground.Iterns.Add(clrbox); Menuitem itemBackground = new MenuItemO; itemBackground.Header = "-Background"; itemColor.Items.Add(itemBackground):
310 Глава 14. Иерархия меню // Создание объекта ColorGrldBox и его привязка // к свойству Background окна clrbox = new ColorGridBoxO: cl rbox. SetBindi ng(ColorGridBox. Sei ectedVa 1 ueProperty, "Background"); clrbox.DataContext = this; itemBackground.Items.Add(clrbox): } } } Эта программа, как и предыдущая, создает объект команды меню верхнего уровня Color и подменю Color с командами Foreground и Background. Но вместо нескольких команд программа добавляет в каждое подменю всего одну коман- ду — объект типа ColorGrldBox. Поскольку класс ColorGrldBox был написан так, что свойство SelectedValue представляет собой объект типа Brush, мы можем полно- стью обойтись без обработчиков событий и просто обеспечить привязку между зависимым свойством SelectedValueProperty объекта ColorGrldBox и свойствами Foreground и Background окна. Мы уже рассмотрели пример использования обработки события SubmenuOpen для пометки команд меню. Обработка этого события особенно часто встречает- ся в программах, в которых требуется заблокировать некоторые команды меню. Типичным примером служит меню правки Edit: в программе с возможностью вставки текста из системного буфера команда Paste должна быть доступна толь- ко в том случае, если буфер действительно содержит текст (а команды Cut, Сору и Delete — только при наличии выделенного фрагмента). Следующая программа реализует меню Edit с командами вырезания, копиро- вания, вставки и удаления текста в элементе TextBlock. Проект также содержит четыре изображения, оформленных в виде ресурсов приложения. Ресурсы ис- пользуются для создания элементов Image, задаваемых свойству Icon каждой ко- манды меню (изображения позаимствованы из библиотеки, входящей в постав- ку Microsoft Visual Studio). CutCopyAndPaste.es // // CutCopyAndPaste.es (с) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; namespace Petzold.CutCopyAndPaste { public class CutCopyAndPaste : Window
Иерархия меню 311 TextBlock text; protected Menuitem ItemCut, ItemCopy, itemPaste, ItemDelete; [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new CutCopyAndPasteO); } public CutCopyAndPasteO { Title = "Cut, Copy, and Paste"; // Создание объекта DockPanel DockPanel dock = new DockPanel(); Content = dock; // Создание меню, пристыкованного у верхнего края окна Menu menu = new Menu О: dock.Children.Add(menu); DockPanel.SetDock(menu, Dock.Top): // Создание объекта TextBlock, заполняющего оставшуюся площадь text = new TextBlock(); text.Text = "Sample clipboard text"; text.Horizontal Alignment = HorizontalAlignment.Center; text.Vertical Alignment = Vertical Allgnment.Center; text.FontSize = 32; text.Textwrapping = Textwrapping.Wrap; dock.Children.Add(text); // Создание меню Edit Menuitem itemEdit = new MenuItemO: itemEdit.Header = "_Edit"; itemEdit.SubmenuOpened += EditOnOpened; menu.Items.Add(itemEdit); // Создание команд меню Edit ItemCut = new MenuItemO; ItemCut.Header = "Cu_t"• ItemCut.Click += CutOnClick; Image img = new ImageO; img.Source = new Bitmaplmage( new Uri("pack://application;,,/Images/CutHS.png")): ItemCut.Icon = img; itemEdit.Items.Add(itemCut); ItemCopy = new MenuItemO; ItemCopy.Header = "_Copy"; ItemCopy.Click += CopyOnClick; img = new ImageO; img.Source = new Bitmaplmage(
312 Глава 14. Иерархия меню new Uni("pack://application:,,/Images/CopyHS.png")); itemCopy.Icon = img; itemEdit.Iterns.Add(itemCopy): itemPaste = new MenuItemO; itemPaste.Header = "_Paste": ItemPaste.Click += PasteOnClick: img = new ImageO: img.Source = new Bitmaplmage( new Uri("pack://application:,,/Images/PasteHS.png")); itemPaste.Icon = img; itemEdit.Items.Add(itemPaste): itemDelete = new MenuItemO: itemDelete.Header = "_Delete"; itemDelete.Click += DeleteOnClick: img = new ImageO; img.Source = new Bitmaplmage( new Uri("pack://application:,,/Images/DeleteHS.png")); itemDelete.Icon = img; itemEdit.Items.Add(itemDelete); } void EditOnOpened(object sender, RoutedEventArgs args) { itemCut.IsEnabled = itemCopy.IsEnabled = itemDelete.IsEnabled = text.Text != null && text.Text.Length > 0; itemPaste. IsEnabled = Clipboard.ContainsTextO; } protected void CutOnClick(object sender, RoutedEventArgs args) { CopyOnClick(sender, args); DeleteOnClick(sender, args): } protected void CopyOnClick(object sender, RoutedEventArgs args) { if (text.Text != null && text.Text.Length > 0) Clipboard.SetTextCtext.Text); } protected void PasteOnClick(object sender, RoutedEventArgs args) { if (Clipboard.ContainsTextO) text.Text = Clipboard.GetTextO: } protected void DeleteOnClick(object sender, RoutedEventArgs args) { text.Text = null: } }
Иерархия меню 313 Программа хранит объекты Menuitem команд Cut, Copy, Paste и Delete в по- лях и обращается к ниАм из обработчика события EditOnOpened. Обработчик де- лает команды Cut, Сору и Delete доступными только в том случае, если TextBlock содержит как минимум один символ текста. Для определения состояния ко- манды Paste используется возвращаемое значение статического метода Clipboard. ContalnsText. Метод PasteOnClick копирует текст из буфера обмена статическим методом Clipboard.GetText. Аналогично, метод CopyOnClick вызывает Clipboard.SetText. Коман- да Delete не нуждается в доступе к клавиатуре, поэтому она просто задает свой- ство Text элемента TextBlock равным null. Обработчик CutOnClick использует тот факт, что операция вырезания состоит из копирования с последующим удале- нием (вызовы CopyOnClick и DeleteOnClick). В меню Edit программы CutCopyAndPaste используется стандартная схема подчеркнутых символов. Например, пользователь может выполнить команду Paste, нажимая клавиши Alt+E и Р. Впрочем, у этих команд также имеются стандарт- ные комбинации клавиш, называемые акселераторами'. Ctrl+X для вырезания, Ctrl+C для копирования, Ctrl+V для вставки и Delete для удаления. В программе CutCopyAndPaste они не реализованы. Вывести текст «Ctrl+Х» рядом с командой Cut несложно. Для этого доста- точно задать свойство InputGestureText команды меню: itemCut.InputGestureText = "Ctrl+X": Однако запуск команды Cut при нажатии клавиш Ctrl+X — совсем иное дело. Автоматически команда выполняться не будет, поэтому возможны два варианта: либо обрабатывайте ввод с клавиатуры самостоятельно (вскоре я покажу, как это делается), либо воспользуйтесь привязкой команд (об этом чуть позже). Если вы решите обрабатывать ввод с клавиатуры самостоятельно, обработка должна производиться с максимально высоким приоритетом. Иначе говоря, не- обходимо проанализировать ввод с клавиатуры на предмет возможных акселе- раторов меню прежде, чем кто-либо другой получит доступ к вводу, — вероятно, для этого придется переопределить метод OnPreviewKeyDown окна. Если нажатая клавиша соответствует доступной команде меню, то команда выполняется, а свой- ство Handled в аргументах события задается равным true. Класс KeyGesture немного упрощает задачу обработки ввода с клавиатуры для инициирования команд меню. Объект типа KeyGesture для комбинации Ctrl+X определяется следующим образом: KeyGesture gestCut = new KeyGesture(Key.X, ModifierKeys.Control); Класс содержит не так уж много полезных функций, и используется он только по одной причине: из-за метода Matches, получающего аргумент InputEventArgs. Метод Matches можно вызвать в переопределении OnPreviewKeyDown и передать ему аргумент KeyEventArgs, полученный с событием (класс KeyEventArgs являет- ся производным от InputEventArgs). Метод Matches «понимает», что его аргу- мент в действительности относится к типу KeyEventArgs, и возвращает true, если
314 Глава 14. Иерархия меню нажатая клавиша совпадает с клавишей, определенной в объекте KeyGesture. Проверка в переопределении OnPreviewKeyDown может выглядеть примерно так: if (gestCut.Matches(null, args)) { CutOnCl1ck(this. args); args.Handled = true; } Объект KeyEventArgs можно передать непосредственно CutOnClick, потому что класс KeyEventArgs наследует от RoutedEventArgs. Тем не менее перед вызовом об- работчика Click этот фрагмент не проверяет, возможно ли выполнение команды вырезания. Казалось бы, простейшее решение проблемы — проверить, доступна ли команда меню itemCut. Однако этот способ не работает, потому что блокировка itemCut устанавливается и снимается только при выводе раскрывающегося меню. К счастью, методы CopyOnClick и PasteOnClick программы CutCopyAndPaste выполняют операции копирования и вставки только в том случае, если коман- ды действительны. Следующая программа использует наследование от CutCopy- AndPaste для реализации стандартных клавиатурных акселераторов меню Edit. Для ее работы необходима ссылка на файл с исходным кодом CutCopyAndPaste.cs. В программе отсутствует директива using для пространства имен этого проекта; вместо этого определение класса ссылается на полностью уточненное имя клас- са CutCopyAndPaste. ControlXCV.cs //-------------------------------------------- // ControlXCV.cs (с) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ControlXCV { public class ControlXCV ; Petzold.CutCopyAndPaste.CutCopyAndPaste { KeyGesture gestCut = new KeyGesture(Key.X. ModifierKeys.Control); KeyGesture gestCopy = new KeyGesture(Key.C, ModifierKeys.Control); KeyGesture gestPaste = new KeyGesture(Key.V, ModifierKeys.Control); KeyGesture gestDelete = new KeyGesture(Key.Delete); [STAThread] public new static void MainO { Application app = new Application); app.Run(new ControlXCVO); } public ControlXCVO
Иерархия меню 315 { Title = "Control X. С, and V"; itemCut.InputGestureText = "Ctrl+X": itemCopy.InputGestureText = "Ctrl+C": itemPaste.InputGestureText = "Ctrl+V"; itemDelete.InputGestureText = "Delete"; } protected override void OnPreviewKeyDown(KeyEventArgs args) { base.OnKeyDown(args); args.Handled = true: if (gestCut.Matches(null, args)) CutOnClick(this. args): else if (gestCopy.Matches(null, args)) CopyOnClick(this. args): else if (gestPaste.Matches(null, args)) PasteOnClick(this, args): else if (gestDelete.Matches(null. args)) DeleteOnClick(this, args); else args.Handled = false: } } } Если при наследовании от класса, определяющего метод Main (такого, как класс CutCopyAndPaste), предоставляется новый метод Main (как в классе ControlXCV), вы должны сообщить Visual Studio, какой из методов Main является настоящей точ- кой входа программы. Откройте окно свойств проекта и укажите в поле Startup Object нужный класс с методом Main. Программа определяет и инициализирует четыре объекта KeyGesture; кроме того, она должна задать соответствующую строку свойству InputGestureText каж- дого объекта Menuitem. (К сожалению, сам класс KeyGesture не предоставляет эту информацию методом ToString или иным способом.) Метод OnPreviewKeyDown сначала устанавливает флаг Handled аргументов со- бытия, а затем сбрасывает его, если клавиша не совпадает ни с одной из опреде- ленных комбинаций. Если программа использует более двух-трех объектов KeyGesture, вероятно, их лучше сохранить в коллекции. Определение поля для создания обобщенного ассоциативного списка (словаря) выглядит так: Dictionary<KeyGesture, RoutedEventHandler> gests = new Dictionary<KeyGesture, RoutedEventHandler>(): Конструктор окна заполняет коллекцию объектами KeyGesture и связанными с ними обработчиками событий: gests.Add(new KeyGesture(Key.X. ModifierKeys.Control). CutOnClick); gests.Add(new KeyGesture(Key.C. ModifierKeys.Control), CopyOnClick);
316 Глава 14. Иерархия меню gests.Add(new KeyGesture(Key.V. ModifierKeys.Control), PasteOnClick): gests.Add(new KeyGesture(Key.Delete), DeleteOnClick); Метод OnPreviewKeyDown перебирает содержимое словаря, находит совпаде- ние и вызывает соответствующий обработчик события: foreach (KeyGesture gest in gests.Keys) if (gest.Matches(null, args)) { gests[gest](this, args): args.Handled = true: } Первая команда блока if индексирует объект gests типа Dictionary объектом gest типа KeyGesture. Результатом индексирования является объект RoutedEventHandler; программа вызывает обработчик, передавая ему в аргументах this и объект Key- EventArgs. Если вы предпочитаете обойтись без прямого вызова обработчиков событий Click, определите объект Dictionary со значениями типа Menuitem: Dictionary<KeyGesture, Menultem> gests = new Dictionary<KeyGesture, Menultem>(); Включение новых данных в словарь производится так: gests.Add(new KeyGesture(Key.X, ModifierKeys.Control). itemCut): Обработка OnKeyDown принимает следующий вид: foreach (KeyGesture gest in gests.Keys) if (gest.Matches(null, args)) gestsfgest].RaiseEventC new RoutedEventArgs(MenuItem.ClickEvent, gestsfgest])): Вероятно, вы уже предположили, что механизм привязки команд обеспечивает более простое решение, и это действительно так. Программа CommandTheButton из главы 4 демонстрирует использование привязки команд с кнопками. С ко- мандами меню привязка работает аналогично. Обычно для этой цели использу- ются статические свойства типа RoutedUICommand класса Applicationcommands и (для более экзотических приложений) классов Componentcommands, EditingCommands, MediaCommands и Navigationcommands, но вы также можете создать собственные команды; далее я покажу, как это делается. Чтобы использовать одно из заранее определенных статических свойств, за- дайте свойство Command класса Menuitem следующим образом: itemCut.Command = Applicationcommands.Cut: Если свойство Header объекта Menuitem не задано, вместо него будет исполь- зоваться свойство Text класса RoutedUICommand, что почти нормально — не счи- тая отсутствия начального символа подчеркивания. Как бы то ни было, объект Menuitem автоматически добавляет текст «Ctrl+Х» в команду меню.
Иерархия меню 317 Другой важный шаг — создание привязки команды на базе объекта Routed- UICommand и ее включение в коллекцию CommandBindings: CommandBi nd1ngs.Add(new CommandBInd1 ng(ApplicationCommands.Cut, CutOnExecute, CutCanExecute)); Привязка команды автоматически обеспечивает обработку стандартных ак- селераторов, связанных с командами. Как будет вскоре показано, акселератор определяется в объекте RoutedUICommand. Привязка связывает команду с собы- тиями CanExecute и Executed класса CommandBinding. При использовании объек- тов RoutedUICommand нет необходимости предоставлять обработчик события только для установления и снятия блокировки команд меню Edit. Управление блокировкой осуществляется через обработчики CanExecute, для чего свойству CanExecute объекта CanExecuteRoutedEventArgs задается значение true или false. Один обработчик CanExecute может обслуживать сразу несколько команд меню. Следующая программа реализует привязку для четырех стандартных команд меню Edit. CommandTheMenu.cs и----------------------------------------------- // CommandTheMenu.cs (с) 2006 by Charles Petzold ц----------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.CommandTheMenu public class CommandTheMenu : Window TextBlock text; [STAThread] public static void MainO Application app = new ApplicationO; app. Run (new CommandTheMenuO); } public CommandTheMenuO { Title = "Command the Menu"; // Создание объекта DockPanel DockPanel dock = new DockPanelО; Content = dock; // Создание меню, пристыкованного у верхнего края окна Menu menu = new Menu О;
318 Глава 14. Иерархия меню dock.Children.Add(menu); DockPanel.SetDock(menu. Dock.Top); // Создание объекта TextBlock, заполняющего оставшуюся площадь text = new TextBlockО: text.Text = "Sample clipboard text"; text.Horizontal Alignment = Horizontal Alignment.Center; text.VerticalAlignment = Vertical Alignment.Center; text.FontSize =32; // 24 пункта text.Textwrapping = Textwrapping.Wrap; dock.Children.Add(text); // Создание меню Edit Menuitem itemEdit = new MenuItemO; itemEdit.Header = "_Edit"; menu.Iterns.Add(i temEdi t); // Создание команд меню Edit Menuitem itemCut = new MenuItemO; itemCut.Header = "Cu_t"; itemCut.Command = Applicationcommands.Cut; itemEdit.Items.Add(itemCut): Menuitem itemCopy = new MenuItemO; itemCopy.Header = "_Copy"; itemCopy.Command = Applicationcommands.Copy; i temEd i t.Iterns.Add(i temCopy); Menuitem itemPaste = new MenuItemO; itemPaste.Header = "_Paste"; itemPaste.Command = Applicationcommands.Paste: itemEdit.Items.Add(itemPaste); Menuitem itemDelete = new MenuItemO; itemDelete.Header = "_Delete"; itemDelete.Command = Applicationcommands.Delete; itemEdit.Items.Add(itemDelete); // Включение привязок команд в коллекцию окна CommandBi nd1ngs.Add(new CommandBIndi ng(Appli cati onCommands.Cut, CutOnExecute, CutCanExecute)): CommandBi ndi ngs.Add(new CommandBi ndi ng(Appli cati onCommands.Copy, CopyOnExecute, CutCanExecute)); CommandBindi ngs.Add(new CommandBi ndi ng(Appli cati onCommands.Paste, PasteOnExecute, PasteCanExecute)); CommandBi ndi ngs.Add(new CommandBi ndi ng(Appli cati onCommands.Delete, DeleteOnExecute, CutCanExecute)); } void CutCanExecute(object sender. CanExecuteRoutedEventArgs args) { args.CanExecute = text.Text != null && text.Text.Length > 0;
Иерархия меню 319 void PasteCanExecute(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = Clipboard.ContainsTextO: } void CutOnExecute(object sender, ExecutedRoutedEventArgs args) { Applicationcommands.Copy.Execute(null, this): ApplicationCommands.Delete.Execute(null, this): } void CopyOnExecute(object sender. ExecutedRoutedEventArgs args) { Cli pboa rd.SetText(text.Text): } void PasteOnExecute(object sender. ExecutedRoutedEventArgs args) { text. Text = Clipboard.GetTextO; } void DeleteOnExecuteCobject sender. ExecutedRoutedEventArgs args) { text.Text = null; } } } Обратите внимание: команды Cut, Copy и Delete используют один обработчик CanExecute. Использовать привязку для реализации стандартных команд Edit удобно, но еще интереснее создавать новые команды. Попробуйте включить следующий фрагмент в конец конструктора в программе CommandTheMenu. Объект создает новую команду с именем Restore, которая восстанавливает исходное содержимое объекта TextBlock. Команде Restore назначается комбинация клавиш Ctrl+R. Отдельная команда RoutedUICommand может быть связана с несколькими комбинациями клавиш, поэтому необходимо определить коллекцию даже в том случае, если вы собираетесь ограничиться всего одной комбинацией: InputGestureCollection collGestures = new InputGestureCollectionO: В коллекцию включается соответствующий объект KeyGesture: coll Gestures.Add(new KeyGesture(Key.R, ModifierKeys.Control)): Затем создается объект RoutedUICommand: RoutedUICommand commRestore = new RoutedUICommand("_Restore", "Restore". GetTypeO. collGestures): Первый аргумент конструктора сохраняется в свойстве Text, а второй — в свой- стве Name. (Обратите внимание на символ подчеркивания, добавленный в свой- ство Text.) В третьем аргументе передается владелец (которым может быть про- сто объект Window), а в четвертом — коллекция объектов KeyGesture.
320 Глава 14. Иерархия меню Остается определить и включить в меню объект Menuitem: Menuitem itemRestore = new MenuItemO: itemRestore.Header = "_Restore": itemRestore.Command = commRestore: itemEdit.Items.Add(itemRestore): Задавать свойство Header не обязательно, потому что ему будет передано свойство Text объекта RoutedUICommand. Также необходимо добавить команду в коллекцию команд окна. Именно на этой стадии определяются обработчики событий: CommandBindings.Add(new CommandBinding(commRestore. RestoreOnExecute)): Обработчик RestoreOnExecute просто восстанавливает исходное состояние объекта TextBlock: void RestoreOnExecute(object sender. ExecutedRoutedEventArgs args) { text.Text = "Sample clipboard text": } Во всех рассмотренных нами программах использовался элемент Menu, обыч- но находящийся у верхнего края окна. В WPF также имеется элемент ContextMenu для представления контекстных меню, обычно вызываемых щелчком правой кнопки мыши. Как и в случае с ToolTip, именем ContextMenu обозначается как свойство, так и класс. И снова по аналогии с ToolTip, свойство ContextMenu определяется обои- ми классами FrameworkElement и FrameworkContentElement. При желании можно определить объект ContextMenu для конкретного элемента, а затем задать его свойству ContextMenu элемента. В этом случае контекстное меню будет откры- ваться щелчком правой кнопкой мыши на этом элементе. Вы можете установить обработчики событий для инициализации меню при открытии, а также обработ- ки различных оповещений. Если свойству ContextMenu элемента не был задан объект ContextMenu, кон- текстное меню придется открывать «вручную» — вероятно, в ответ на событие MouseRightButtonUp. К счастью, открыть контекстное меню совсем несложно; для этого достаточно задать его свойство IsOpen равным true. По умолчанию контекстное меню отображается в позиции указателя мыши. Следующая программа похожа на пример ToggleBoldAndltalic из главы 3. Она выводит знаменитую фразу; пользователь может щелкать правой кнопкой мыши на каждом слове. На экране появляется контекстное меню с командами форма- тирования (жирный шрифт, курсив и т. д.). Программа создает только один объект ContextMenu, используемый со всеми словами текста, и не пытается отслеживать форматирование каждого слова. Вместо этого контекстное меню инициализируется атрибутами форматирования того слова, на котором был сделан щелчок.
Иерархия меню 321 popupContextMenu.cs // popupContextMenu.es (с) 2006 by Charles Petzold и using System; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media: namespace Petzold.PopupContextMenu { public class PopupContextMenu : Window { ContextMenu menu; Menuitem itemBold, itemltalic; Menultemf] itemOecor: Inline inlClicked: [STAThread] public static void MainO Application app = new Application!): app.Run(new PopupContextMenu()) • } public PopupContextMenu() { Title = "Popup Context Menu"; // Создание объекта ContextMenu menu = new ContextMenuО: // Добавление команды "Bold". itemBold = new MenuItemO; itemBold.Header = "Bold": menu.I terns.Add(itemBold); // Добавление команды "Italic" itemltalic = new MenuItemO; itemltalic.Header = "Italic": menu.Items.Add(itemltalic); // Получение всех данных TextDecorationLocation TextDecorationLocation[] Iocs = (TextDecorationLocation[]) Enum.GetVaiues(typeof(TextDecorati onLocat i on)):
322 Глава 14. Иерархия меню // Создание массива объектов Menuitem и его заполнение ItemDecor = new Menultem[locs.Length]; for (int 1 = 0; 1 < Iocs.Length; 1++) { TextDecoratlon decor = new TextDecoratlonO; decor.Location = locs[1]; 1temDecor[1] = new MenuItemO; 1temDecor[1]. Header = locs[1].ToStrlngO; 1temDecor[1].Tag = decor; menu.Items.Add(1temDecor[1]); } // Один обработчик используется для всех контекстных меню menu.AddHandler(Menu Itern.Cl 1ckEvent, new RoutedEventHandler(MenuOnCllck)); // Создание объекта TextBlock как содержимого окна TextBlock text = new TextBlockO; text.FontSize = 32; text.HorizontalAlignment = HorizontalAlignment.Center; text.Vertical Allgnment = VerticalAlignment.Center; Content = text; // Разбиение знаменитой цитаты на слова string strQuote = "То be. or not to be. that Is the question"; str1ng[] strWords = strQuote.SplItO; // Создание объекта Run для каждого слова // и его включение в TextBlock foreach (string str In strWords) { Run run = new Run(str); run.TextDecoratlons = new TextDecorat1onCollect1on(); text.Inllnes.Add(run); text.IniInes.AddC "); } } protected override void OnMouseRightButtonUp(MouseButtonEventArgs args) { base.OnMouseRIghtButtonUp(args); If ((InlCUcked = args.Source as Inline) != null) { // Пометка команд меню в соответствии // со значениями свойств Inline ItemBold.IsChecked = (Ini СИ eked.FontWeight == FontWeights.Bold): 1temltal1c.IsChecked = (InlCUcked.Fontstyle == Fontstyles. Italic);
Иерархия меню 323 foreach (Menuitem item in itemDecor) item.IsChecked = (ini Cl 1 eked.TextDecorations.Contains (item.Tag as TextDecoration)); // Отображение контекстного меню menu.IsOpen = true; args.Handled = true; } } void MenuOnCl1ck(object sender, RoutedEventArgs args) { Menuitem item = args.Source as Menuitem; item.IsChecked true; // Изменение объекта Inline в зависимости // от установленной или снятой пометки if (item == itemBold) inlClicked.FontWeight = (item.IsChecked ? FontWeights.Bold ; FontWeights.Normal); else if (item == itemltalic) inlClicked.Fontstyle = (item.IsChecked ? Fontstyles.Italic ; FontStyles.Normal); else { if (item.IsChecked) inlClicked.TextDecorations.Adddtem.Tag as TextDecoration): else ini Clicked.TextDecorations.Removed tern.Tag as TextDecoration); } (inlClicked.Parent as TextBlock).InvalIdateVisual(); } } } Первая часть конструктора окна посвящена созданию объекта ContextMenu. После включения в меню команд Bold и Italic конструктор окна получает все чле- ны перечисления TextDecorationLocation (Underline, Overline, Strikethrough и Baseline). Конструктор использует метод AddHandler объекта ContextMenu для назначения одного обработчика Click всем командам меню. Метод Split класса String делит фразу на слова, которые преобразуются в объ- екты типа Run и объединяются в объекте TextBlock. Обратите внимание на то, как для каждого объекта Run явно создается объект TextDecorationCollection. По умол- чанию коллекция не существует, а свойство TextDecorations обычно равно null. Хотя метод OnMouseRightButtonUp вроде бы доставляет события мыши окну, механизм маршрутизации событий гарантирует, что при щелчке на объекте Inline свойство Source в аргументах события будет ссылаться именно на этот объект (вспомните, что класс Run является производным от Inline). Затем обра- ботчик события инициализирует меню по свойствам слова, на котором был сде- лан щелчок.
324 Глава 14. Иерархия меню MenuOnClick вручную переключает свойство IsChecked объекта, на котором был сделан щелчок. Строго говоря, это не обязательно, потому что меню ис- чезает при щелчке, но обработчик события использует повое значение свойства IsChecked для изменения форматирования объекта Inline, на котором был сделан щелчок. Как упоминалось в начале главы, меню располагается у верхнего края окна. Обычно под меню находится панель инструментов, а у нижнего края окна — строка состояния. Эти элементы окна будут описаны в следующей главе.
1g* Панель инструментов iwl и строка состояния До недавнего времени меню и панели инструментов было легко отличить друг от друга. Мешо содержало иерархический набор текстовых команд, а панели ин- струментов — ряды кнопок с графическими изображениями. Но когда в меню стали появляться значки и элементы, а на панелях инструментов — раскрываю- щиеся меню, различия стали менее очевидными. Традиционно панели инстру- ментов размещались у верхнего края окна прямо под меню, но в действительно- сти оии могут быть пристыкованы к любому краю окна. Класс ToolBar является производным от HeaderedltemsControl, как и классы Menuitem и TreeViewItem (см. следующую главу). А это означает, что ToolBar со- держит коллекцию Items, в которой хранятся кнопки и другие элементы, отобра- жаемые на панели. Кроме того, класс ToolBar содержит свойство Header, но с го- ризонтальными панелями инструментов оно обычно не используется. Специального класса ToolBarltem не существует. На панели инструментов раз- мещаются те же элементы, что и в окнах и на панелях. Конечно, чаще всего используются кнопки, на которых обычно отображаются небольшие значки. Элемент ToggleButton используется для представления параметров с двумя со- стояниями (вкл/выкл). На панелях инструментов также могут располагаться элементы ComboBox и однострочные элементы TextBox. На панель даже хможно поместить объект Menuitem с раскрывающимся списком команд или других эле- ментов управления. Элемент Separator обеспечивает деление содержимого пане- ли на функциональные группы. Панели инструментов обычно содержат больше графики и меньше текста, чем диалоговые окна, поэтому размещаемые на них элементы рекомендуется снабжать экранными подсказками. Если элемент панели инструментов дублиру- ет команду мешо, для них обычно используется одна привязка команды. Далее приводится довольно бесполезная программа, которая создает панель инструментов и размещает на ней 8 кнопок. Программа определяет массив из восьми статических свойств класса Applicationcommands (типа RoutedUICommand) и соответствующий массив из восьми имен графических файлов, находящихся в каталоге Images проекта. Каждой кнопке назначается один из объектов Routed- UICommand (свойство Command) и графическое изображение.
326 Глава 15. Панель инструментов и строка состояния CraftTheToolbar.cs //------------------------------------------------- // CraftTheToolbar.cs (с) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Windows: using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; namespace Petzold.CraftTheToolbar { public class CraftTheToolbar : Window { [STAThread] public static void MainO { Application app = new Application); app.Run(new CraftTheToolbar()); } public CraftTheToolbar() { Title = "Craft the Toolbar"; RoutedUICommandE] comm = { Appli cati onCommands.New, Appli cati onCommands.Open. ApplicationCommands.Save, ApplicationCommands.Pri nt, ApplicationCommands.Cut, ApplicationCommands.Copy, ApplicationCommands.Paste. ApplicationCommands.Delete }: string[] strimages = { "NewDocumentHS.png", "openHS.png", "saveHS.png". "PrintHS.png", "CutHS.png". "CopyHS.png". "PasteHS.png", "DeleteHS.png" // Создание объекта DockPanel как содержимого окна DockPanel dock = new DockPanel О; dock.LastChildFill = false; Content = dock; // Создание панели инструментов, пристыкованной // у верхнего края окна Tool Bar toolbar = new Tool Bar();
Панель инструментов и строка состояния 327 dock.Children.Add(tool ba г); DockPanel.SetDock(toolbar, Dock.Top): // Создание кнопок панели инструментов for (int 1 = 0: 1 < 8: 1++) { if (i ==» 4) toolbar. I terns. Add (new SeparatorO): // Создание объекта Button Button btn = new ButtonO; btn.Command = commfi]; toolbar.Items.Add(btn): // Создание объекта Image как содержимого Button Image img = new ImageO: img.Source = new Bitmaplmage( new Uri("pack://application:,,/Images/" + strlmages[i])): img.Stretch = Stretch.None; btn.Content = img; // Создание объекта ToolTip на основе текста UlCommand ToolTip tip = new ToolTipO; tip.Content = commfi].Text; btn.ToolTip = tip; // Включение UlCommand в привязки команд окна CommandBindings.Add( new CommandBinding(comm[i], ToolBarButtonOnClick)); } } // Фиктивный обработчик команды для кнопки void ToolBarButtonOnClick(object sender, ExecutedRoutedEventArgs args) { RoutedUICommand comm = args.Command as RoutedUICommand; MessageBox.Show(comm.Name + " command not yet implemented". Title): } } } При щелчке на любой из кнопок обработчик события ToolBarButtonOnClick вы- водит окно с сообщением. Этот обработчик связывается с объектами RoutedUI- Command при включении команд в коллекцию привязок окна в последней коман- де цикла for. Обратите внимание: свойство Text каждого объекта RoutedUICommand используется при создании элемента ToolTip, связанного с кнопкой.
328 Глава 15. Панель инструментов и строка состояния Приятным побочным эффектом привязки команд является клавиатурный интерфейс. При нажатии клавиш Ctrl+N, Ctrl+O, Ctrl+S, Ctrl+P, Ctrl+X, Ctrl+C, Ctrl+V и Delete на экране появляется окно сообщения. Изображения взяты из библиотеки, входящей в поставку Microsoft Visual Studio 2005. Они имеют форму квадрата со стороной 1G пикселов. В отдельных случаях мне пришлось воспользоваться графическим редактором, чтобы при- вести изображение к разрешению в 96 точек на дюйм. В контексте Windows Presentation Manager это означает, что изображения представляют собой квад- рат со стороной 1/6 дюйма. На мониторах с высоким разрешением изображения сохранят примерно те же физические размеры, хотя и будут выглядеть менее четко. Интересный эксперимент: попробуйте удалить команду, предотвращающую заполнение клиентской области панелью инструментов. dock.LastChi1dFi11 = false; Теперь добавьте элемент RichTextBox на панель DockPanel. Этот фрагмент мож- но разместить непосредственно перед циклом for: RichTextBox txtbox = nev/ RichTextBox(): dock.ChiIdren.Add(txtbox): Также для этого эксперимента очень важно, чтобы фокус ввода передавался объекту RichTextBox в самом конце конструктора окна: txtbox.Focus О; Как известно, объект RichTextBox обрабатывает комбинации клавиш Ctrl+X, Ctrl+C и Ctrl+V для реализации команд вырезания, копирования и вставки (все три команды присутствуют в контекстном меню, открываемом щелчком правой кнопкой мыши на RichTextBox). Однако привязки команд, реализуемые RichTextBox, правильно взаимодействуют с привязками команд окна: при отсутствии выде- ленного текста в RichTextBox кнопки Cut и Сору блокируются! Если выделить текст и щелкнуть на одной из кнопок, вместо вывода окна сообщения будет вы- полнена операция (хотя если кнопки заблокированы, RichTextBox игнорирует на- жатия Ctrl+X и Ctrl+C, а программа выводит окно сообщения). Даже при отсутствии помощи RichTextBox программа, реализующая кнопку Paste, должна устанавливать и снимать блокировку кнопки в зависимости от со- держимого буфера. Если программа откажется от использования стандартных объектов RoutedUICommand, ей придется установить таймер для проверки содер- жимого буфера (например, каждую десятую секунды) и разрешать пли запре- щать доступ к кнопке в зависимости от результата проверки. Работать со стан- дартными объектами RoutedUICommand обычно проще, потому что при каждом изменении содержимого буфера вызывается обработчик CanExecute, ассоцииро- ванный с привязкой команды. Некоторые программы (такие, как Microsoft Internet Explorer) выводят пояс- нительный текст рядом с теми кнопками, смысл которых не очевиден. С пане-
Панель инструментов и строка состояния 329 лями инструментов WPF задача легко решается при помощи панели StackPanel. Сначала закомментируйте следующую команду в цикле if: btn.Content = img: Вставьте следующий фрагмент сразу же после этой команды: StackPanel stack = new StackPanel О; stack.Orientation = Orientation.Horizontal; btn.Content = stack; TextBlock txtblk = new TextBlockO: txtblk.Text = commfi].Text; stack.Children.Add(ling); stack.Children.Add(txtblk): Теперь справа от изображения на каждой кнопке выводится текст. Чтобы текст выводился под изображением, задайте StackPanel вертикальную ориента- цию. Порядок Image и TextBlock изменяется простой перестановкой команд, до- бавляющих элементы в коллекцию дочерних объектов StackPanel. Обратите внимание па небольшой «корешок» у левого края панели инст- рументов. Если провести над ним указателем мыши, последний принимает вид Cursors.SizeAII (четыре стрелки), но несмотря па визуальные признаки, перемес- тить панель инструментов не удастся. Вскоре я покажу, как это сделать. Если задать свойство Header объекта ToolBar, соответствующий текст (или другой объ- ект) будет отображаться между корешком и первым элементом на панели. Если ширина окна недостаточна для размещения всей панели инструментов, небольшая кнопка у правого края панели открывает временное окно со скрыты- ми кнопками. Это окно представлено объектом типа ToolBarOverflowPanel. Кроме того, для размещения объектов па самой панели используется вспомогательный класс ToolBarPanel. Иерархия всех классов, связанных с панелями инструментов, выглядит так: FrameworkElement Control ItemsControl HeaderedItemsControl ToolBar Panel (абстрактный) StackPanel ToolBarPanel ToolBarOverflowPanel ToolBarTray Класс ToolBarTray играет важную роль при реализации нескольких панелей инструментов, а также при использовании вертикальных панелей инструментов. Его применение продемонстрировано в программе MoveTheToolbar. Программа создает два таких объекта: один пристыкован у верхнего, а другой — у левого края клиентской области. В каждой области ToolBarTray программа создает три элемента ToolBar с заголовками (просто для того, чтобы их было удобнее разли- чать) и шестью кнопками.
330 Глава 15. Панель инструментов и строка состояния MoveTheToolbar.cs //----------------------------------------------- // MoveTheToolbar.cs (с) 2006 by Charles Petzold //----------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.MoveTheToolbar { public class MoveTheToolbar : Window { [STAThread] public static void MalnO { Application app = new ApplicationO; app.Run(new MoveTheToolbarO); } public MoveTheToolbarO { Title = "Move the Toolbar"; // Создание объекта DockPanel как содержимого окна DockPanel dock = new DockPanel 0; Content = dock; // Создание областей ToolBarTray у левого и верхнего края ToolBarTray trayTop = new ToolBarTrayО; dock.Chi 1dren.Add(trayTop); DockPanel.SetDock(trayTop. Dock.Top); ToolBarTray trayLeft = new ToolBarTrayO; trayLeft.Orientation = Orientation.Vertical; dock.Chi 1 dren.Add(trayLeft); DockPanel.SetDock(trayLeft, Dock.Left); // Создание объекта TextBox. заполняющего остальную площадь // клиентской области TextBox txtbox = new TextBoxO; dock.Children.Add(txtbox); // Создание 6 панелей инструментов for (Int 1 = 0; 1 < 6; 1++) { ToolBar toolbar = new ToolBarO; tool bar.Header = "Toolbar " + (i + 1): if (1 < 3)
Панель инструментов и строка состояния 331 tгауТop.Tool Ba гs.Add(tool ba г); else trayLeft.Tool Bars.Add(tool bar); // ... и шести кнопок на каждой панели for (int j = 0; j < 6: J++) { Button btn = new ButtonO; btn.FontSize = 16; btn.Content = (char)('A' + j); toolbar.Iterns.Add(btn); } } } } } Обратите внимание: области ToolBarTray, пристыкованной у левого края окна, назначается вертикальная ориентация. Все элементы ToolBar, размещаемые в этой области, отображают свое содержимое по вертикали. Программа задает свой- ству Header каждого объекта ToolBar числовое значение. Панели инструментов с номерами 1, 2 и 3 находятся в верхней области ToolBarTray, а панели 4, 5 и 6 — в левой области. В границах каждой области можно перемещать панели инструментов так, чтобы они следовали друг за другом в одной строке/столбце, или же выстроить их в несколько строк/столбцов. Переместить панели инструментов из одной об- ласти ToolBarTray в другую невозможно. Для задания начальной раскладки панелей инструментов, а также для сохра- нения раскладок, выбранных пользователем, используются два целочисленных свойства ToolBar с именами Band и Bandindex. Для горизонтальных панелей инст- рументов свойство Band обозначает строку, занимаемую панелью, a Bandindex — позицию в этой строке (от левого края). Для вертикальных панелей инструмен- тов свойство Band обозначает столбец, a Bandindex — позицию в этом столбце от верхнего края. По умолчанию всем панелям инструментов задается свойство Band, равное 0, а значения Bandindex нумеруются последовательно от 0 в поряд- ке включения панелей в область ToolBarTray. В оставшейся части этой главы я хотел бы продемонстрировать применение панелей инструментов в более «жизненных» примерах. Программа FormatRichText обходится без меню, но содержит четыре панели инструментов и строку состоя- ния. Панели инструментов обеспечивают файловые операции, работу с буфером обмена, форматирование символов и форматирование абзацев. Как обычно, класс FormatRichText, содержащий основной объем кода, объявлен производным от Window. Но я хотел, чтобы файлы с исходным кодом остава- лись относительно небольшими, а логически связанные фрагменты были сгруп- пированы, поэтому класс FormatRichText был разбит на шесть частей с использо- ванием ключевого слова partial. Части класса хранятся в разных файлах. Первый файл называется FormatRichText.cs, тогда как код открытия/сохранения файлов
332 Глава 15. Панель инструментов и строка состояния хранится в файле FormatRichText.File.cs. Для работы проекта также необходима ссылка на файл ColorGridBox.cs из главы 11. FormatRichText.cs //------------------------------------------------ // FormatR1chText.cs (с) 2006 by Charles Petzold //------------------------------------------------ using System; using System.Windows; us1 ng System.WIndows.Controls: using System.Windows.Input; using System.Windows.Media: namespace Petzold.FormatRIchText public partial class FormatRIchText : Window { RichTextBox txtbox; [STAThread] public static void Ma1n() { Application app = nev/ Appl 1 cat 1 on(); app.Run(new FormatRIchText()); } public FormatR1chText() { Title = "Format Rich Text": // Создание объекта DockPanel как содержимого окна DockPanel dock = new DockPanel(); Content = dock; // Создание области ToolBarTray у верхнего края окна ToolBarTray tray = new ToolBarTray(); dock.ChiIdren.Add(tray); DockPanel.SetDock(tray, Dock.Top); // Создание объекта RichTextBox txtbox = new R1chTextBox(); txtbox.Vert1ca1Scrol1BarVIsi bl 11ty = ScrollBarVlsIblllty.Auto; // Вызов методов из других файлов AddFI1 eTool Bar(tray. 0. 0): AddEd1tToolBar(tray, 1, 0): AddCharToolBar(tray, 2, 0): AddParaToolBar(tray, 2, 1); AddStatusBar(dock);
Панель инструментов и строка состояния 333 // Создание объекта RichTextBox, заполняющего // остальную площадь, и передача ему фокуса dock.ChiIdren.Add(txtbox): txtbox.Focus(); } } Конструктор создает объекты DockPanel, ToolBarTray и RichTextBox. Обратите внимание: объект RichTextBox сохраняется в поле, чтобы он было доступен для других частей класса FormatRichText. Затем конструктор вызывает пять методов (AddFileToolBar и т. д.), каждый из которых находится в отдельном файле. Во вто- ром и третьем аргументах методов передаются нужные значения свойств Band и Bandindex элемента ToolBar. Второй файл проекта — FormatRichText.File.cs — посвящен загрузке и сохране- нию файлов. Элемент RichTextBox позволяет загружать и сохранять файлы в че- тырех разных форматах, соответствующих четырем статическим полям класса DataFormats: DataFormats.Text, DataFormats.Rtf (Rich Text Format), DataFormats.Xaml и DataFormats.XamIPackage (ZIP-файл, содержащий другие файлы). Эти форматы перечислены в массиве, определяемом в виде ноля в начале файла. Обратите внимание па повторение DataFormats.Text в конце файла — пять форматов соот- ветствуют пяти секциям strFilter, необходимым классам OpenFileDialog и SaveFile- Dialog для отображения различных типов файлов и расширений в стандартных диалоговых окнах. FormatRichText.File.cs //----------------------------------------------------- // FormatRichText.File.cs (с) 2006 by Charles Petzold //----------------------------------------------------- using Microsoft.Win32: using System; using System.10: using System.Windows: using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input: using System.Windows.Media: using System.Windows Media.Imaging: namespace Petzold.FormatRichText public partial class FormatRichiext : Window { stringU Formats = { DataFormats.Xaml. DataFormats.XamlPackage. DataFormats.Rtf, DataFormats.Text. DataFormats.Text
334 Глава 15. Панель инструментов и строка состояния string strFilter = "XAML Document Files (*.xaml)|*.xaml|" + "XAML Package Files (*.zip)|*.zip|" + "Rich Text Format Files (*.rtf)|*.rtf|" + "Text Files (*.txt)|*.txt|" + "All files (*.*)|*.*"; void AddFileToolBarCToolBarTray tray, int band, int index) { // Создание объекта ToolBar ToolBar toolbar = new ToolBarO: tool bar.Band = band: tool bar.Bandindex = index: tray.Tool Ba rs.Add(toolbar): RoutedUICommand[] comm = { ApplicationCommands.New, Appli cati onCommands.Open, Appli cati onCommands.Save }; string[] strimages = { "NewDocumentHS.png", "openHS.png", "saveHS.png" }; // Создание кнопок для панели инструментов for (int i = 0: i < 3: i++) Button btn = new ButtonO: btn.Command = comm[i]; toolbar.Items.Add(btn): Image img = new ImageO: img.Source = new Bitmaplmage( new Uri("pack://application:,,/Images/" + strImages[i])); img.Stretch = Stretch.None: btn.Content = img: ToolTip tip = new ToolTipO: tip.Content = comm[i].Text: btn.ToolTip = tip; } // Добавление привязок команд CommandBindings.Add( new CommandBinding(ApplicationCommands.New, OnNew)): CommandBindings.Add( new CommandBinding(Applicationcommands.Open, OnOpen)): CommandBindings.Add( new CommandBinding(Applicationcommands.Save. OnSave)): }
Панель инструментов и строка состояния 335 // New: содержимое инициализируется пустой строкой void OnNewtobject sender, ExecutedRoutedEventArgs args) { FlowDocument flow = txtbox.Document: TextRange range = new TextRange(flow.Contentstart, flow.ContentEnd); range.Text = : } // Open: вызов диалогового окна и загрузка файла void OnOpen(object sender, ExecutedRoutedEventArgs args) { OpenFileDialog dig = new OpenFileDialog(): dlg.CheckFileExists = true; dig.Filter = strFilter; if ((bool)dlg.ShowDialog(this)) { FlowDocument flow = txtbox.Document; TextRange range = new TextRange(flow.Contentstart, flow.ContentEnd): FileStream strm = null: try л strm = new FileStream(dlg.FileName, Fi1 eMode.Open): range.Load(strm, formats[dlg.Filterindex - 1]); } catch (Exception exc) MessageBox.Show(exc.Message, Title): finally { if (strm != null) strm.Closet): } } } // Save: вызов диалогового окна и сохранение файла void OnSavetobject sender, ExecutedRoutedEventArgs args) { SaveFileDialog dig = new SaveFileDialogO; dig.Filter = strFilter; if ((bool)dlg.ShowDialog(this)> { FlowDocument flow = txtbox.Document: TextRange range = new TextRange(flow.Contentstart, flow.ContentEnd): FileStream strm = null; try
336 Глава 15. Панель инструментов и строка состояния { stem = new Fl 1eStream(dlg.Fl 1eName. Fl1 eMode.Create); range.Save(strm, formats[dlg.Filterindex - ]]); } catch (Exception exc) { MessageBox.Show(exc.Message. Title); finally If (strm != nul1) strin.Close(): } } } } Метод AddFileToolBar создает объект ToolBar и три объекта Button, соответствую- щих стандартным командам Applicationcommands.New, Applicationcommands.Open и Applicationcommands.Save. (Программа FormatRichText реагирует на комбина- ции Ctrl+N, Ctrl+O и Ctrl+S — стандартные акселераторы для этих команд.) Чтобы не усложнять программу, я опустил некоторые второстепенные удоб- ства. Программа не запоминает имя файла, поэтому она не может реализовать команду Save простым сохранением файла под текущим именем. Кроме того, она не предупреждает о наличии в файле несохраненных изменений. Впрочем, эти отсутствующие возможности реализованы в программе Notepadclone в главе 18. Методы OnOpen и OnSave похожи друг на друга. Оба метода вызывают стан- дартное диалоговое окно, а когда пользователь нажимает кнопку открытия или сохранения файла — получают объект FlowDocument от объекта RichTextBox, после чего создают объект TextRange, соответствующий всему содержимому докумен- та. Класс TextRange содержит методы Load и Save; первый аргумент относится к типу Stream, во втором передается поле из DataFormats. Методы индексируют массив форматов по свойству Filterindex диалогового окна. Свойство обознача- ет секцию strFilter, выбранную пользователем перед нажатием кнопки (обратите внимание: Filterindex уменьшается па 1, потому что значения свойства начинают- ся с 1, а не с 0). Следующий файл обрабатывает команды, обычно находящиеся в меню Edit. Эксперименты с программой CraftTheToolbar в начале главы показывают, что привязки команд, добавляемые в окно, подключаются к привязкам команд для дочернего объекта, обладающего фокусом ввода, то есть RichTextBox. Код в файле создает кнопки и привязки команд, а также добавляет привязки команд в коллек- цию окна, однако большая часть команд не реализована. Большая часть логики работы с буфером обмена обеспечивается самим клас- сом RichTextBox.
Панель инструментов и строка состояния 337 FormatRichText.Edit.cs И----------------------------------------------------- И FormatRIchText.Edit.cs (с) 2006 by Charles Petzold ------------------------------------------------------ using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media; using System.Wi ndows.Media.Imaging; namespace Petzold.FormatRichText public partial class FormatRichText : Window { void AddEditToolBar(ToolBarTray tray, int band, int index) { // Создание объекта Toolbar ToolBar toolbar = new ToolBarO; tool bar.Band = band: toolbar.Bandindex = index: tray.Tool Bars.Add(tool bar): RoutedUICommand[] comm = { Appli cati onCommands.Cut. Appli cati onCommands.Copy, ApplicationCommands.Paste, ApplicationCommands.Delete, ApplicationCommands.Undo. ApplicationCommands.Redo stringE] strimages = { "CutHS.png", "CopyHS.png", "PasteHS.png", "DeleteHS.png", "Edit_UndoHS.png", "Edit_RedoHS.png" for (int i = 0; i < 6; i++) if (i == 4) toolbar.Items.Add(new Separator()): Button btn = new ButtonO: btn.Command = commEi]; toolbar.Iterns.Add(btn): Image img = new ImageO; img.Source = new Bitmaplmage( new Uri("pack;//application:,./Images/" + strlmagesEi])): img.Stretch = Stretch.None: btn.Content = img; ToolTip tip = new ToolTipO: tip.Content = commEi].Text;
338 Глава 15. Панель инструментов и строка состояния btn.ToolTip = tip; } CommandBindings.Add(new CommandBi nding( Appli cati onCommands.Cut)); CommandBi ndi ngs.Add(new CommandBi ndi ng( Appli cati onCommands.Copy)): CommandBindings.Add(new CommandBi nding( Appli cati onCommands.Paste)); CommandBindi ngs.Add(new CommandBi ndi ng( ApplicationCommands.Delete. OnDelete, CanDelete)); CommandBi ndi ngs.Add(new CommandBi ndi ng( Appli cati onCommands.Undo)); CommandBi ndings.Add(new CommandBi ndi ng( Applicationcommands.Redo)); void CanDelete(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = !txtbox.Select!on.IsEmpty: } void OnDelete(object sender. ExecutedRoutedEventArgs args) { txtbox.Select!on.Text = : } } Правильно работать отказывалась только клавиша Delete, поэтому я добавил обработчики CanExecute и Executed для этой команды. Объект RichTextBox поддер- живает кнопки отмены и возврата (Undo и Redo) постоянно доступными, что не совсем правильно. Но когда я попытался реализовать обработчики CanExecute для этих команд с вызовами методов CanUndo и CanRedo класса RichTextBox, ока- залось, что эти методы всегда возвращают true! Самое интересное начинается в следующем файле. В нем определяется объ- ект ToolBar, управляющий форматированием символов: шрифт, размер шрифта, жирное и курсивное начертание, основной цвет и цвет фона. Элементы панели должны отображать название шрифта и другие атрибуты форматирования, свя- занные с выделенным фрагментом текста или (при его отсутствии) текущей по- зицией курсора. Для получения текущего выделения в RichTextBox используется свойство Selection. Это свойство относится к типу Textselection, который факти- чески представляет собой объект TextRange. Класс TextRange определяет два ме- тода с именами GetPropertyValue и ApplyPropertyValue. В первом аргументе обоих методов передается зависимое свойство, участвующее в форматировании. Например, для получения семейства шрифтов, связанного с текущим выде- ленным текстом в объекте txtBox типа RichTextBox, используется следующая ко- манда: txtbox.Seiecti on.GetPropertyValue(FlowDocument.FontFamilyProperty);
Панель инструментов и строка состояния 339 Обратите внимание: мы получаем свойство объекта FlowDocument, представ- ляющего содержимое RichTextBox. Но что произойдет, если в текущем выделении участвуют несколько семейств шрифтов (или других атрибутов форматирова- ния символов или абзацев)? Этот вопрос недостаточно хорошо документирован. Проверьте, не равно ли возвращаемое значение null, и также убедитесь в том, что оно относится именно к тому типу, который вас интересует. Например, задание нового объекта FontFamily (например, fontfam) для текущего выделения (или те- кущей позиции) осуществляется вызовом следующего вида: txtbox.Seiection.ApplyPropertyValue(FlowDocument.FontFanm lyProperty. fontfam); Чтобы элементы панели инструментов обновлялись, необходимо установить обработчик события SelectionChanged объекта RichTextBox. Для отображения семейства шрифтов и размера шрифта на панели символь- ного форматирования используются элементы ComboBox. Такой элемент пред- ставляет собой комбинацию TextBox и ListBox, причем вы можете управлять тем, на какой из двух прототипов он должен быть больше похож. Класс ComboBox, как и ListBox, является производным от Selector. Для заполне- ния ComboBox используется коллекция Items или свойство ItemsSource. ComboBox наследует свойства Selectedlndex, Selectedltem и SelectedValue от класса Selector. В отличие от ListBox он не поддерживает режима множественного выделения. В обычном состоянии в ComboBox выводится всего одна строка текста, доступ- ная для чтения и записи через свойство Text. Кнопка у правого края ComboBox вызывает раскрывающийся список (dropdown) отображаемых элементов. ComboBox определяет свойства MaxDropDownHeight и IsDropDownOpen, доступные для чтения и записи, а также два события DropDownOpen и DropDownClosed. ComboBox определяет важное свойство IsEditable, по умолчанию равное false. В этом режиме в верхней части ComboBox отображается выделенный объект (если он есть). Если щелкнуть на нем, раскрывающийся список открывается или за- крывается. Пользователь не может ввести информацию в этом поле, но если ввести с клавиатуры букву, в поле может быть выделен объект, начинающийся с указанной буквы. Программа может прочитать или записать текстовое пред- ставление выделенного объекта через свойство Text, но, вероятно, будет надеж- нее использовать Selectedltem. Если программа пытается задать свойству Text значение, не соответствующее одному из объектов в списке, ComboBox не будет иметь выделенного объекта. Если программа задает свойству IsEditable истинное значение, верхняя часть ComboBox превращается в текстовое поле, в котором пользователь может вводить данные. Тем не менее не существует события, сообщающего об изменении текста. У ComboBox также имеется третий режим, полезность которого весьма огра- ничена: когда программа задает истинное значение IsEditable, она также может задать истинное значение IsReadOnly. В этом случае пользователь не может из- менить содержимое текстового поля, но может выделить его для копирования в буфер. При ложном свойстве IsEditable значение IsReadOnly игнорируется. Практическое применение ComboBox порой сопряжено с некоторыми про- блемами, даже когда свойство IsEditable сохраняет значение по умолчанию false
340 Глава 15. Панель инструментов и строка состояния (например, как объект ComboBox для семейства шрифтов — бессмысленно вво- дить название семейства шрифтов, отсутствующего в списке). Возникает соблаз- нительная идея — просто назначить обработчик события SelectionChanged объекта ComboBox и завершить обработку возвратом фокуса ввода объекту RichTextBox; именно так я реализовал ComboBox для выбора шрифта. Такое решение работает нормально, если пользователь щелкает на стрелке рядом со списком, а затем щелкает на одном из семейств шрифтов. Оно работает нормально и в том слу- чае, если пользователь щелкает на стрелке рядом с объектом ComboBox, а затем прокручивает список с клавиатуры и нажимает Enter для выделения объекта (или Escape для отмены). Тем не менее у этого простого решения есть свои недостатки: предполо- жим, пользователь щелкает на текстовой части ComboBox, а затем щелкает па ней снова, чтобы закрыть список. Фокус ввода должен остаться у ноля ComboBox. Теперь пользователь нажимает клавиши Т и Ф для прокрутки списка. Должен ли выделенный текст в RichTextBox изменяться в соответствии с выделенным шрифтом? Вероятно, должен, и я полагаю, что объект ComboBox в это время дол- жен сохранять фокус ввода. Но предположим, пользователь нажимает клавишу Escape. Выделенный текст в RichTextBox должен вернуться к прежнему шрифту, выбранному до открытия ComboBox, а фокус ввода с клавиатуры должен пе- рейти от ComboBox к RichTextBox. Кроме того, фокус ввода должен возвращать- ся к RichTextBox и при нажатии пользователем клавиши Enter, по в этом случае текущее значение из ComboBox должно быть применено к выделенному тексту RichTextBox. Реализация этой логики (как обычно говорят учителя-садисты) пре- доставляется читателю для самостоятельной работы. С объектом ComboBox для выбора размера шрифта дело обстоит еще хуже. Традиционно такой объект должен содержать набор стандартных размеров шриф- тов, но при этом пользователю обычно предоставляется возможность ввести дру- гое значение. Следовательно, свойство IsEditable объекта ComboBox должно быть истинным. Но как узнать о завершении ввода? Пользователь должен иметь воз- можность покинуть ComboBox клавишей Tab (в этом случае фокус ввода переда- ется следующему элементу ToolBar), Enter (фокус передается RichTextBox) пли Escape (фокус передается RichTextBox, a ComboBox возвращается к состоянию до редактирования). Пользователь также может покинуть ComboBox, щелкнув кноп- кой мыши где-то в другом месте. Все перечисленные действия сопряжены с поте- рей фокуса, поэтому следующая программа устанавливает обработчик события LostKeyboa rd Focus объекта ComboBox. Обработчик пытается вызовом Double.Try- Parse преобразовать текст, введенный пользователем, в число. Если попытка преобразования завершается неудачей, текст восстанавливается в исходном со- стоянии. Для сохранения исходного состояния необходим обработчик события GotKeyboardFocus. Кроме того, для обработки клавиш Enter и Escape потребуется обработчик PreviewKeyDown. FormatRichText.Char.cs //--------------------------------------------------- // FormatRichText.Char.cs (с) 2006 by Charles Petzold //---------------------------------------------------
Панель инструментов и строка состояния 341 using Petzold.SelectColorFromGrid; // для ColorGrldBox using System; using System.Windows: using System.Windows.Controls; using System.Windows.Controls.Primitives: using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; namespace Petzold.FormatRichText public partial class FormatRichText : Window { ComboBox comboFamily, comboSize; ToggleButton btnBold, btnltalic; ColorGrldBox clrboxBackground. clrboxForeground; void AddCharToolBar(ToolBarTray tray, int band, int index) { // Создание объекта ToolBar и его размещение в ToolBarTray ToolBar toolbar = new ToolBarO; tool bar.Band = band; toolbar.Bandindex = index; tray.Tool Bars.Add(tool bar); // Создание объекта ComboBox для семейств шрифтов comboFamily = new ComboBoxO; comboFamily.Width = 144; comboFamily.ItemsSource = Fonts.SystemFontFamilies; comboFamily.Selectedltem = txtbox.FontFamily; comboFamily.SelectionChanged += FamilyComboOnSelection; toolbar.Items.Add(comboFamily); ToolTip tip = new ToolTipO; tip.Content = "Font Family"; comboFamily.ToolTip = tip; // Создание объекта ComboBox для размера шрифта comboSize = new ComboBoxO; comboSize.Width = 48; comboSize.IsEditable = true; comboSize.Text = (0.75 * txtbox.FontSize) .ToStringO ; comboSize.ItemsSource = new doublet] { 8. 9. 10. 11. 12. 14. 16. 18. 20. 22. 24. 26. 28. 36. 48. 72 comboSize.SelectionChanged += SizeComboOnSelection; comboSize.GotKeyboardFocus += SizeComboOnGotFocus;
342 Глава 15. Панель инструментов и строка состояния comboSize.LostKeyboardFocus += SizeComboOnLostFocus: comboSize.PreviewKeyDown += SizeComboOnKeyDown; toolbar.Items.Add(comboSize); tip = new ToolTipO; tip.Content = "Font Size"; comboSize.Tool Tip = tip: // Создание кнопки жирного начертания btnBold = new ToggleButtonO; btnBold.Checked += BoldButtonOnChecked; btnBold.Unchecked += BoldButtonOnChecked: toolbar.Items.Add(btnBold): Image Img = new ImageO: img.Source = new Bitmaplmage( new Uri("pack-.//application:, ,/Images/boldhs.png")): img.Stretch = Stretch.None: btnBold.Content = img: tip = new ToolTipO; tip.Content = "Bold": btnBold.Tool Tip = tip: // Создание кнопки курсивного начертания btnltalic = new ToggleButtonO: btnltalic.Checked += ItalicButtonOnChecked; btnltalic.Unchecked += ItalicButtonOnChecked: toolbar.Items.Add(btnltalic): img = new ImageO: img.Source = new Bitmaplmage( new Uri("pack://application:, ,/Images/ItalicHS.png")): img.Stretch = Stretch.None; btnltalic.Content = img: tip = new ToolTipO; tip.Content = "Italic"; btnltalic.ToolTip = tip: toolbar.Items.Add(new SeparatorO): // Создание меню цвета текста и фона Menu menu = new MenuO: toolbar.Items.Add(menu); // Создание меню Background
Панель инструментов и строка состояния 343 Menu Item item = new MenuItemO: menu.Iterns.Add(item); img = new Image(): img.Source = new Bitmaplmage( new Uri("pack://application:,,/Images/ColorHS.png")); img.Stretch = Stretch.None; item.Header = img: clrboxBackground = new ColorGridBox(); clrboxBackground.SeiectionChanged += BackgroundOnSelectionChanged: item.Items.Add(clrboxBackground); tip = new ToolTipO: tip.Content = "Background Color": item.ToolTip = tip; // Создание меню Foreground item = new MenuItemO: menu.Iterns.Add(item); img = new ImageO: img.Source = new Bitmaplmage( new Uri("pack://application:,./Images/Color_fontHS.png")): img.Stretch = Stretch.None; item.Header = img; clrboxForeground = new ColorGridBoxO; clrboxForeground.Seiecti onChanged += ForegroundOnSelecti onChanged; item.I terns.Add(clrboxForeground): tip = new ToolTip(); tip.Content = "Foreground Color"; item.Tool Tip = tip: // Установка обработчика события SelectionChanged // для объекта RichTextBox txtbox.SelectionChanged += TextBoxOnSelectionChanged; } // Обработчик события SelectionChanged объекта RichTextBox void TextBoxOnSelectionChanged(object sender, RoutedEventArgs args) { // Получение семейства шрифтов для выделенного текста... object obj = txtbox.Selection.GetPropertyValue(
344 Глава 15. Панель инструментов и строка состояния FlowDocument.FontFarni lyProperty): II... и инициализация ComboBox. if (obj is FontFamily) comboFamily.Selectedltem = (FontFamily)obj; else comboFamily.Selectedlndex = -1; // Получение размера шрифта для выделенного текста... obj - txtbox.Selection.GetPropertyValue( FlowDocument.FontSi zeProperty); // ... и инициализация ComboBox. if (obj is double) comboSize.Text = (0.75 * (double)obj).ToString(): else comboSize.Selected Index = -1; // Получение насыщенности шрифта выделенного текста... obj = txtbox.Select!on.GetPropertyValue( FlowDocument.FontWeightProperty); // .. и инициализация объекта ToggleButton if (obj is FontWeight) btnBold.IsChecked = (FontWeight)obj == Fontweights.Bold; // Получение свойства Fontstyle выделенного текста... obj = txtbox.Select!on.GetPropertyValue( FlowDocument.FontSty1 eProperty); // .. и инициализация объекта ToggleButton if (obj is Fontstyle) btnltalic.IsChecked = (FontStyle)obj == Fontstyles.Italic: // Получение цветов и инициализация элементов ColorGridBox obj = txtbox.Selection.GetPropertyValue( FlowDocument.Backgroundproperty); if (obj != null && obj is Brush) clrboxBackground.SelectedValue = (Brush)obj; obj = txtbox.Selection.GetPropertyValue( FlowDocument.Foregroundproperty): if (obj != null && obj is Brush) clrboxForeground.SelectedValue = (Brush)obj; // Обработчик события SelectionChanged объекта ComboBox void FamilyComboOnSelection(object sender,
Панель инструментов и строка состояния 345 SelectionChangedEventArgs args) { // Получение выбранного семейства шрифтов ComboBox combo = args.Source as ComboBox: FontFamily family = combo.Selectedltem as FontFamily; // Применение семейства шрифтов к выделенному тексту If (family != null) txtbox.Seiectlon.ApplyPropertyVa1ue( FlowDocument.FontFaml1yProperby. family): // Возвращение фокуса объекту TextBO/< txtbox.Focus(): // Обработчики для объекта ComboBox, управляющего размером шрифта string strOrlglnal: void SIzeComboOnGotFocus(object sender, KeyboardFocusChangedEventArgs args) { strOrlglnal = (sender as ComboBox).Text: } void SizeComboOnLostFocus(object sender. KeyboardFocusChangedEventArgs args) { double size: If (Double.TryParse((sender as ComboBox).Text, out size)) txtbox.Seiectlon.ApplyPropertyValue( FlowDocument.FontSIzeProperty. size / 0.75): else (sender as ComboBox).Text = strOriginal: void SizeComboOnKeyDown(object sender. KeyEventArgs args) { If (args.Key == Key.Escape) { (sender as ComboBox).Text = strOrlglnal: args.Handled = true: txtbox. FocusO: } else If (args.Key =-= Key.Enter) args.Handled = true: txtbox.Focus();
346 Глава 15. Панель инструментов и строка состояния void SizeComboOnSelection(object sender, SelectionChangedEventArgs args) { ComboBox combo = args.Source as ComboBox; if (combo.Selectedlndex != -1) { double size = (double) combo.SelectedValue; txtbox.Selection.ApplyPropertyVa1ue( FlowDocument.FontSizeProperty, size / 0.75); txtbox.Focus(): } } // Обработчик для кнопки Bold void BoldButtonOnChecked(object sender, RoutedEventArgs args) { ToggleButton btn = args.Source as ToggleButton; txtbox.Selection.ApplyPropertyValue( FlowDocument.FontWei ghtProperty, (bool)btn.IsChecked ? FontWeights.Bold ; FontWeights.Normal); } // Обработчик для кнопки Italic void ItalicButtonOnChecked(object sender. RoutedEventArgs args) { ToggleButton btn = args.Source as ToggleButton; txtbox.Seiecti on.ApplyPropertyVa1ue( F1owDocument.FontSty1 eProperty. (bool)btn.IsChecked ? Fontstyles.Italic ; FontStyles.Normal); } // Обработчик изменения цвета фона void BackgroundOnSelectionChanged(object sender. SelectionChangedEventArgs args) { ColorGrldBox clrbox = args.Source as ColorGrldBox; txtbox.Seiecti on.ApplyPropertyValue( FlowDocument.Backgroundproperty, clrbox.SelectedValue); } // Обработчик изменения цвета текста void ForegroundOnSelectionChanged(object sender. SelectionChangedEventArgs args) ColorGrldBox clrbox = args.Source as ColorGrldBox;
Панель инструментов и строка состояния 347 txtbox.Seiectj on.ApplуPropertyVaiue( FlowDocument.Foregroundproperty, clrbox.SelectedValue); } } Оставшаяся часть выглядит относительно тривиально. Метод AddCharToolBar также создает два объекта ToggleButton (для жирного и курсивного начертания) и меню для основного и фонового цвета. Да, панель инструментов содержит полноценное меню, но оно состоит всего из двух команд. У каждого из этих двух объектов Menuitem свойству Header задается растровое изображение, пред- ставляющее основной или фоновый цвет, а коллекция Items содержит только элемент ColorGridBox. Панель форматирования абзацев на фоне предыдущей панели выглядит эле- ментарно, потому что она содержит всего четыре изображения, определяющих тип выравнивания. Найти подходящие изображения мне не удалось, поэтому пришлось создавать их прямо в программе. Метод CreateButton размещает на ToggleButton панель Canvas в виде квадрата со стороной 16 единиц и рисует 5 ли- ний, представляющих различные типы выравнивания. FormatRichText.Para.cs И----------------------------------------------------- И FormatRichText.Para.cs (с) 2006 by Charles Petzold И----------------------------------------------------- using System; using System.Windows: using System.Windows.Controls: using System.Windows.Controls.Primitives: using System.Windows.Documents: using System.Windows.Input; using System.Windows.Media: using System.Windows.Shapes: namespace Petzold.FormatRi chText { public partial class FormatRichText : Window { ToggleButton[] btnAHgnment = new ToggleButton[4]; void AddParaToolBar(ToolBarTray tray, int band, int index) { // Создание объекта ToolBar и его размещение в ToolBarTray ToolBar toolbar = new ToolBarO; toolbar.Band = band: toolbar.Bandindex = index; tray.Tool Bars.Add(tool bar);
348 Глава 15. Панель инструментов и строка состояния // Создание элементов toolbar.Items.Add(btnAlignment[O] = CreateButton(TextAlignment.Left, "Align Left", 0. 4)); toolbar.Items.Add(btnAlignment[l] = CreateButton(TextAlignment.Center, "Center", 2, 2)); toolbar.Items.Add(btnAlignment[2] = CreateButton(TextAlignment.Right, "Align Right", 4, 0)); toolbar.Items.Add(btnAlignment[3] = CreateButton(TextAlignment.Justify, "Justify", 0, 0)); // Установка другого обработчика события SelectionChanged txtbox.SelectionChanged += TextBox0nSelectionChanged2; } ToggleButton CreateButton(TextAlignment align, string strToolTip, int offsetLeft, int offsetRight) { // Создание объекта ToggleButton ToggleButton btn = new ToggleButton(); btn.Tag = align; btn.Click += ButtonOnClick: // Создание панели Canvas и ее назначение содержимым кнопки Canvas canv = new CanvasO; canv.Width = 16: canv.Height = 16; btn.Content = canv; // Рисование линий на объекте Canvas for (int i = 0; i < 5: i++) { Polyline poly = new PolylineO: poly.Stroke = SystemColors.WindowTextBrush; poly.StrokeThickness = 1: if ((i & 1) == 0) poly.Points = new PointCollection(new Point[] { new Point(2. 2 + 3 * i). new Point(14. 2 + 3*1) }): else poly.Points = new PointCollection(new Point[] new Point(2 + offsetLeft, 2 + 3 * i), new Point(14 - offsetRight, 2 + 3 * i) }): canv.Children.Add(poly): } // Создание объекта ToolTip ToolTip tip = new ToolTipO;
Панель инструментов и строка состояния 349 tip.Content = strToolTip; btn.ToolTip = tip: return btn; } // Обработчик события SelectionChanged объекта TextBox void TextBox0nSelectionChanged2(object sender, RoutedEventArgs args) // Получение текущего значения TextAlignment object obj = txtbox.Selection.GetPropertyValue( Paragraph.TextAlignmentProperty): // Инициализация кнопок if (obj != null && obj is TextAlignment) { TextAlignment align = (TextAlignment)obj: foreach (ToggleButton btn in btnAlignment) btn.IsChecked = (align == (TextAlignment)btn.Tag): } else foreach (ToggleButton btn in btnAlignment) btn.IsChecked = false: } } // Обработчик события Click объекта Button void ButtonOnClick(object sender. RoutedEventArgs args) { ToggleButton btn = args.Source as ToggleButton; foreach (ToggleButton btnAlign in btnAlignment) btnAlign.IsChecked = (btn == btnAlign); // Задание нового значения TextAlignment TextAlignment align = (TextAlignment) btn.Tag; txtbox.Selection.ApplyPropertyValue( Paragraph.TextAlignmentProperty, align): } } } На этом логика работы панели инструментов завершается. Программа Format- RichText также содержит строку состояния. Класс StatusBar является производ- ным от ItemsControl (как Menu), a StatusBarltem — от Contentcontrol (как Menuitem); Control Contentcontrol StatusBarltem ItemsControl StatusBar
350 Глава 15. Панель инструментов и строка состояния Строка состояния обычно стыкуется у нижнего края клиентской области. В реальных приложениях строка состояния обычно содержит только текст, ино- гда элемент ProgressBar (когда требуется загрузить или сохранить большой файл или выполнить другую продолжительную операцию). Во внутреннем представ- лении StatusBar использует DockPanel для формирования макета, поэтому если в вашем приложении строка состояния содержит несколько объектов, вы може- те вызвать метод DockPanel.SetDock для их размещения. В этой программе строка состояния используется для вывода текущей даты и времени. Format RichText.Status.cs //------------------------------------------------------- // FormatRichText.Status.cs (c) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input: using System.Windows.Media; using System.Windows.Thread!ng; namespace Petzold.FormatRi chText { public partial class FormatRichText : Window { StatusBarltem itemDateTime: void AddStatusBar(DockPanel dock) // Создание строки состояния, пристыкованной у нижнего края окна StatusBar status = new StatusBarO; dock.Children.Add(status); DockPanel.SetDock(status, Dock.Bottom); // Создание объекта StatusBarltem itemDateTime = new StatusBarItem(); itemDateTime.Horizontal Alignment = HorizontalAlignment.Right; status.Items.Add(itemDateTime); // Создание таймера для обновления StatusBarltem DispatcherTimer tmr = new DispatcherTimerO; tmr.Interval = TimeSpan.FromSeconds(l); tmr.Tick += TimerOnTick; tmr.Start(); } void TimerOnTick(object sender. EventArgs args) {
Панель инструментов и строка состояния 351 DateTime dt = DateTime.Now; ItemDateTime.Content = dt.ToLongDateStringO + "" + dt.ToLongT1meString(); } } Программа FormatRichText имитирует немалую часть функций приложения WordPad, но я не собираюсь совершенствовать ее. В главе 18 будет представлен клон редактора Блокнот (Notepad). Конечно, эта задача гораздо проще, но зато потраченные усилия окупятся в главе 20, когда программа будет взята за основу при разработке полезной утилиты XamiCruncher.
16 TreeView и Listview Элемент TreeView предназначен для отображения иерархических данных. Веро- ятно, самым известным примером использования TreeView является панель в ле- вой части Проводника Windows, на которой отображаются все диски и каталоги пользователя. Иерархическое дерево также присутствует в левой части окна про- граммы Microsoft Document Viewer, предназначенной для просмотра документа- ции Microsoft Visual Studio и .NET. Каждый логический элемент (узел) объекта TreeView представляет собой объект типа TreeViewItem. Обычно объект TreeViewItem идентифицируется корот- кой текстового строкой, но при этом содержит коллекцию вложенных объектов TreeViewItem. Таким образом, по своей структуре TreeView сильно напоминает Menu, а объекты TreeViewItem очень близки к Menuitem, как видно из следующей выборочной иерархии классов, включающей все основные элементы из двух предыдущих глав: Control ItemsControl HeaderedltemsControl Menuitem ToolBar TreeViewItem MenuBase (абстрактный) ContextMenu Menu StatusBar TreeView Напомню, что ItemsControl также является родительским классом для Selector — родительского класса ListBox и ComboBox. ItemsControl содержит важное свойство Items — коллекцию объектов, перечисляемых в элементе. Класс Headeredltems- Control добавляет к ItemsControl свойство с именем Header. Хотя свойство относит- ся к типу Object, очень часто его содержимым является обычная текстовая строка. По аналогии с классом Menu, который представляет коллекцию объектов Menuitem верхнего уровня (и поэтому не имеет свойства Header), класс TreeView представляет коллекцию объектов TreeViewItem и тоже не имеет свойства Header.
TreeView и Listview 353 Следующая программа заполняет элемент TreeView «вручную», то есть по спи- ску жестко фиксированных логических элементов. ManuallyPopulateTreeView.cs // ManuallyPopulateTreeView.es (с) 2006 by Charles Petzold И using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ManuallyPopulateTreeView { public class ManuallyPopulateTreeView : Window ( [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new ManuallyPopulateTreeView()); } public ManuallyPopulateTreeView() { Title = "Manually Populate TreeView"; TreeView tree = new TreeViewO; Content = tree; TreeViewItem itemAnimal = new TreeVi ewItemO; itemAnimal.Header = "Animal"; tree.Iterns.Add(itemAnimal); TreeViewItem itemDog = new TreeViewltem(); itemDog.Header = "Dog"; i temDog.Items.Add("Poodle"); itemDog.Items.Add("Irish Setter"); itemDog.Items.Add("German Shepherd"); itemAnimal.Iterns.Add(itemDog); TreeViewItem itemCat = new TreeViewItem(); itemCat.Header = "Cat"; itemCat.Items.Add("Calico"); TreeViewItem item = new TreeViewItem(); item.Header = "Alley Cat"; i temCat.Iterns.Add(item);
354 Глава 16. TreeView и ListView Button btn = new ButtonO: btn.Content = "Noodles": itemCat.Iterns.Add(btn); itemCat.Items.Add("Siamese"): itemAnima1.Items.Add(itemCat): TreeViewItem itemPrimate = new TreeViewItem(); itemPrimate.Header = "Primate": itemPrimate. Items.AddCChimpanzee"); itemPrimate.Items.Add("Bonobo"): itemPrimate.Items.Add("Human"): itemAnimal.Iterns.Add(itemPrimate); TreeViewItem itemMineral = new TreeViewItem(); itemMineral.Header = "Mineral": itemMineral.Items.Add("Calcium"): itemMineral.Items.AddC'Zinc"): i temMi nera1.1terns.Add("Iron"): t гее.Iterns.Add(i temMi neral): TreeViewItem itemvegetable = new TreeViewItem(): itemVegetable.Header = "Vegetable": itemVegetable. Items. AddCCarrot"); itemVegetable.Items.Add("Asparagus"): i temVegetable.Iterns.Add("Broccoli"): tree.Items.Add(itemVegetable): } } } Программа демонстрирует два разных способа включения объектов TreeViewItem в родительский объект. Можно явно создать объект типа TreeViewItem и доба- вить его в объект ItemCollection, обозначенный свойством Items. Такой способ не- обходим при наличии дочерних объектов. Но если объект не имеет дочерних объектов, возможно другое, намного более простое решение — передать тексто- вую строку методу Add коллекции Items. Программа позволяет поэкспериментировать с интерфейсом TreeView. Щел- чок на значке «+» раскрывает ветвь, а щелчок на значке «-» сворачивает ее. Эле- мент TreeView также обладает полноценным клавиатурным интерфейсом, осно- ванном на клавишах со стрелками. В остальном программа не демонстрирует сколько-нибудь полезных обобщен- ных приемов программирования TreeView. Если заполнение элементов Menu по- средством ручного добавления команд и подменю является стандартной практи- кой, подобное заполнение TreeView в реальных приложениях встречается крайне редко. Гораздо чаще элементы TreeView заполняются на основании содержимого баз данных или других источников информации, внешних по отношению к про- грамме.
TreeView и Listview 355 Например, для заполнения элемента TreeView, предназначенного для отображе- ния дисков и каталогов, используются классы из пространства имен System.10. Следующая программа конструирует элемент TreeView с информацией обо всех каталогах текущего системного диска. В имени программы кроется предупреждение: в ней представлен неверный способ заполнения TreeView деревом каталогов! RecurseDirectoriesInefficiently.cs // // RecurseDirectorieslnefficiently.cs (с) 2006 by Charles Petzold // using System; using System.10; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.RecurseDi rectori esInetfi ci ent1 у { public class RecurseDirectoriesInefficiently : Window { [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new RecurseDirectorieslnefficiently()); public RecurseDirectoriesInefficiently() Title = "Recurse Directories Inefficiently"; TreeView tree = new TreeViewO; Content = tree; // Создание объекта TreeViewItem для системного диска TreeViewItem item = new TreeViewItem(); item.Header = Path.GetPathRoot(Environment.SystemDirectory); item.lag = new Directorylnfo(item.Header as string): tree.Items.Add(item); // Рекурсивное заполнение GetSubdi rectori es(i tern); } void GetSubdirectoriesCTreeViewItem item) { Directoryinfo dir = item.Tag as Di rectoryInfo; Directorylnfo[] subdirs;
356 Глава 16. TreeView и ListView try { // Получение подкаталогов subdirs = di г.GetDIrector1es(); } catch { return; } // Перебор подкаталогов foreach (Directoryinfo subdlr In subdlrs) { // Создание нового объекта TreeViewItem для каждого каталога TreeViewItem subItem = new TreeV1ewItem(); subItem.Header = subdlr.Name: subItem.Tag = subdlr; Item.Items.Add(subltem); // Рекурсивное получение подкаталогов GetSubdlrectories(subItem); } } } } Теоретически все выглядит хорошо. Конструктор окна создает элемент TreeView и первый объект TreeViewItem. Свойству Header объекта задается корневой путь текущего системного диска (например, текстовая строка вида «С:\», а свойству Тад — объект Directoryinfo, созданный на базе той же строки. Затем конструктор передает этот объект TreeViewItem методу GetSubdirectories. Метод GetSubdirectories является рекурсивным. Он использует метод GetDirec- tories объекта Directoryinfo для получения всех дочерних подкаталогов (блок try- catch необходим на случай обнаружения каталога, доступ к которому запрещен). Для каждого подкаталога метод создает новый объект TreeViewItem, а затем вы- зывает GetSubdirectories с новым объектом. Со структурной точки зрения именно так программировались утилиты командной строки, предназначенные для выво- да деревьев каталогов. Конечно, решение, подходящее для командной строки, может оказаться ужас- ным для графической среды. Если вы рискнете запустить эту программу, оказы- вается, что она подолгу работает с диском перед отображением окна. Проблема даже не в задержке, а в том, что программа делает гораздо больше необходимо- го — вряд ли пользователь захочет просмотреть все без исключения каталоги, добавляемые программой в TreeView. Более эффективный элемент TreeView для отображения дерева каталогов загружает информацию только тогда, когда она потребуется — например, когда пользователь щелкает на значке «+» рядом с име- нем каталога. Впрочем, это слишком поздно; значок «+» отображается только при наличии дочерних объектов, поэтому программа должна опережать действия
TreeView и Listview 357 пользователя на один шаг. Для каждого отображаемого узла дерева информация об узлах следующего уровня должна присутствовать, но для узлов более низких уровней немедленная загрузка не обязательна. Описанная методика носит название отложенной загрузки. Чтобы правильно реализовать отложенную загрузку для TreeView, необходимо хорошо знать четы- ре основных события TreeViewItem. Событие Expanded сообщает о щелчках на значке «+» для отображения дочерних узлов, а также о ручной установке свой- ства IsExpanded. Свойство Collapse имеет обратный смысл, то есть сообщает о сво- рачивании ветвей. События Selected и Unselected сообщают о выделении узла пользователем и об изменении свойства IsSelected. Все четыре события облада- ют сопутствующими методами On. Сам объект тоже TreeView обладает событием SelectedltemChanged. Однако свойство Selectedltem объекта TreeView доступно только для чтения, поэтому для задания исходного выделенного узла TreeView на программном уровне сле- дует задать свойство IsSelected самого объекта TreeViewItem. Давайте посмотрим, удастся ли нам использовать эти события для реализации нормального работающего дерева каталогов. Чтобы было интереснее, при выде- лении каталога в дереве будет выводиться список файлов. А чтобы было еще ин- тереснее, в TreeView будут использоваться графические изображения. Для вывода графики из каталога outline\16color_nomask графической библиотеки Visual Studio необходим белый фон. Некоторые изображения пришлось изменить под разре- шение устройства 96 DPI. Как и в Проводнике Windows, при выделении ката- лога изображение закрытой папки сменяется изображением открытой папки. Проект начинается с класса ImagedTreeViewItem, производного от TreeViewItem. ImagedT reeViewItem.es И И ImagedTreeViewItem.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.RecurseDi rector1esIncrementally { public class ImagedTreeViewItem : TreeViewItem { TextBlock text; Image img; ImageSource srcSelected, srcllnselected; // Конструктор создает панель StackPanel с изображением и текстом public ImagedTreeViewItemO
358 Глава 16. TreeView и ListView StackPanel stack = new StackPanel0; stack.Orientation = Orientation.Horizontal: Header = stack; img = new ImageO; img.VerticalAlignment = Vertical Alignment.Center; img.Margin = new Thickness(O, 0, 2, 0); stack.Children.Add(img); text = new TextBlockO; text.VerticalAlignment = Vertical Alignment.Center; stack.Children.Add(text); } // Открытые свойства текста и изображений public string Text { set { text.Text = value; } get { return text.Text; } public ImageSource Selectedlmage { set { srcSelected = value; if (IsSelected) img.Source = srcSelected: } get { return srcSelected; } } public ImageSource Unselected Image { set { srcUnselected = value; if (’IsSelected) img.Source = srcUnselected; } get { return srcUnselected; } } // Переопределения событий для задания изображений protected override void OnSelected(RoutedEventArgs args) { base.OnSelected(args); img.Source = srcSelected; } protected override void OnUnselected(RoutedEventArgs args) {
TreeView и Listview 359 base.OnUnselected(args); img. Source = srcUnselected; } } } Класс ImagedTreeViewItem задает свойству Header, унаследованному от TreeView- Item, объект StackPanel. Дочерними объектами StackPanel являются объекты Image и TextBlock. Открытое свойство Text позволяет пользователю элемента задать свойство Text объекта TextBlock. Два других открытых свойства относятся к типу ImageSource; одно называется Selectedlmage (используется при выделении объек- та), а другое Unselectedlmage (используется при отсутствии выделения). Переопределения методов OnSelected и Onllnselected отвечают за задание свой- ству Source объекта Image правильного объекта ImageSource. Класс DirectoryTreeViewItem наследует от ImagedTreeViewItem и предоставляет два изображения, необходимых для его базового класса. Его конструктор полу- чает параметр Directoryinfo, который сохраняется в поле и делается доступным через открытое свойство Directoryinfo. (DirectoryTreeViewItem можно представить себе как визуальную обертку для объекта Directoryinfo.) Обратите внимание: конструктор также задает свойство Text, определяемое в ImagedTreeViewItem. DirectoryTreeViewItem.cs и И DirectoryTreeViewItem.cs (с) 2006 by Charles Petzold // using System; using System.10; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media: using System.Windows.Media.Imaging: namespace Petzold.RecurseDirectoriesIncrementally { public class DirectoryTreeViewItem: ImagedTreeViewItem { Di rectoryInfo dir: // Конструктор получает объект Directoryinfo public DirectoryTreeViewItem(DirectoryInfo dir) { this.dir = dir; Text = dir.Name; Selectedlmage = new Bitmaplmage( new Uri("pack://application:../Images/OPENFOLD.BMP")); Unselectedlmage = new Bitmaplmage(
360 Глава 16. TreeView и ListView new Uri("pack://application:../Images/CLSDFOLD.BMP")); } // Открытое свойство для получения объекта Directoryinfo public Directoryinfo Directoryinfo { get { return dir: } } // Открытый метод для заполнения узлов public void PopulateO { Directorylnfo[] dirs; try { dirs = dir.GetDirectoriesO: } catch { return; } foreach (Directoryinfo dirChild in dirs) Iterns.Add(new Di rectoryTreeViewltem(dirChi1d)); } // Переопределение события для заполнения вложенных узлов protected override void OnExpanded(RoutedEventArgs args) { base.OnExpanded(args); foreach (object obj in Items) { DirectoryTreeViewItem item = obj as DirectoryTreeViewItem; item.Populate(); } } } Открытый метод Populate получает все подкаталоги объекта Directoryinfo, свя- занного с узлом, и создает новые объекты DirectoryTreeViewItem, представляющие его дочерние узлы. В первоначальном варианте я определил Populate как приват- ный метод, который вызывался из конструктора. Вскоре вы увидите, почему пришлось сделать его открытым и отказаться от вызова по умолчанию. Класс также переопределяет метод OnExpanded. В сущности, метод предна- значен для отображения всех дочерних подкаталогов. На момент отображения все эти подкаталоги должны располагать информацией о своих подкаталогах, чтобы правильно отображать (или не отображать) значки «+». Это и делает ме- тод OnExpanded.
TreeView и Listview 361 Класс DirectoryTreeView является производным от TreeView. Он отвечает за по- лучение списка всех дисков и за начало процесса создания объектов Directory- TreeViewItem. DirectoryTreeView.es и // DirectoryTreeView.cs (с) 2006 by Charles Petzold // using System; using System.10; using System.Windows; using System.Windows.Controls; using System.Windows.Input: using System.Windows.Media: using System.Windows.Media.Imaging; namespace Petzold.RecurseDi rectorieslncrementally { public class DirectoryTreeView : TreeView { // Конструктор строит неполное дерево каталогов oublic Di rectory!reeView() '{ RefreshTree(): } public void RefreshTreeO { Beginlnit(); Items.ClearO; // Получение информации о дисках Drivelnfo[] drives = Driveinfo.GetDrives(); foreach (Driveinfo drive in drives) { char chDrive = drive.Name.ToUpper()[0]; DirectoryTreeViewItem item = new DirectoryTreeViewItem(drive.RootDirectory); // Если диск готов, вывести метку тома; в противном случае // выводится только тип диска if (chDrive != 'А' && chDrive != 'В' && drive.IsReady && drive.VolumeLabel.Length > 0) item.Text = String.Format("{0} ({1})". drive.VolumeLabel. drive.Name); else item.Text = String.Format("{0} ({1})". drive.DriveType, drive.Name); // Выбор изображения для диска
362 Глава 16. TreeView и ListView if (chDrive == 'A' || chDrive == *B’) item.Selectedlmage = item.Unselectedlmage = new Bitmaplmage( new Uri("pack://application:,, /Images/35FL0PPY.BMP")); else if (drive.DriveType == DriveType.CDRom) item.Selectedlmage = item.Unselectedlmage = new Bitmaplmage( new Uri("pack://application:.,/Images/CDDRIVE.BMP")); else item.Selectedlmage = item.Unselectedlmage = new Bitmaplmage( new Uri("pack://application:.,/Images/DRIVE.BMP")); Iterns.Add(item): // Заполнение информации о каталогах if (chDrive != 'A' && chDrive != ’B‘ && drive.IsReady) item.Populate(): } Endlnit(): } } } Я включил в класс DirectoryTreeView открытый метод RefreshTree, который мо- жет вызываться командой меню Refresh; в этом примере он просто вызывается из конструктора. Метод RefreshTree переходит к блоку инициализации вызовом Beginlnit, удаляет все узлы и создает их заново. После вызова Beginlnit элемент TreeView не пытается постоянно обновлять свое визуальное представление, пото- му что это может замедлить построение дерева. Метод RefreshTree получает массив объектов Driveinfo с описанием всех дис- ков на компьютере. Свойство RootDirectory класса Driveinfo является объектом типа Directoryinfo, что позволяет легко использовать его для создания объекта DirectoryTreeViewItem. Тем не менее с дисками А и В необходимо действовать ос- торожно. Конечно, нам не хотелось бы, чтобы при инициализации элемента TreeView начинали вращаться флоппи-дисководы. Хуже этого может быть толь- ко одно: назойливые сообщения об отсутствии диска! По этой причине програм- ма не трогает свойство IsReady объектов Driveinfo для флоппи-дисководов. Также обратите внимание, что метод вызывает Populate для дискового устройства только в том случае, если последнее не является флоппи-дисководом, а свойство IsReady равно true. Последний класс проекта — RecurseDirectoriesIncrementally — является произ- водным от Window. RecurseDirectoriesIncrementally.cs // // RecurseDirectorieslncrementally.cs (с) 2006 by Charles Petzold // using System; using System.10; using System.Windows;
TreeView и ListView 363 using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: namespace Petzold.RecurseDi rectoriesIncrementally class RecurseDirectorieslncrementally : Window { StackPanel stack; [STAThread] public static void MainO { Application app = new ApplicationO: app.Run(new RecurseDirectorieslncrementally()): } public RecurseDirectorieslncremental1у() { Title = "Recurse Directories Incrementally": // Создание объекта Grid как содержимого окна Grid grid = new Grid(): Content = grid; // Определение объектов ColumnDefinition ColumnDefinition coldef = new ColumnDefinition(); coldef.Width = new GridLength(50, GridUnitType.Star); grid.ColumnDefi ni ti ons.Add(col def): coldef = new ColumnDefinition(): col def.Width = GridLength.Auto: grid.ColumnDefi ni ti ons.Add(col def); coldef = new ColumnDefinition(); coldef.Width = new GridLength(50, GridUnitType.Star); grid.ColumnDefi ni ti ons.Add(col def); // Объект DirectoryTreeView размещается слева DirectoryTreeView tree = new DirectoryTreeViewO: tree.SeiectedltemChanged += TreeViewOnSelectedltemChanged; grid.Chi 1dren.Add(tree); Grid.SetColumn(tree. 0); // Объект GridSplitter размещается в центре GridSplitter split = new GridSplitter(): split.Width = 6: split.ResizeBehavior = GridResizeBehavior.PreviousAndNext; grid.Chi 1dren.Add(split): Grid.SetColumn(split. 1): // Панель StackPanel с прокруткой размещается справа Scroll Vi ewer scroll = new Scrol1 Viewer():
364 Глава 16. TreeView и ListView grid.Children.Add(scroll); Grid.SetColumn(scroll, 2); stack = new StackPanel 0; scroll.Content = stack; } void TreeViewOnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> args) { // Получение выделенного узла DirectoryTreeViewItem item = args.NewValue as DirectoryTreeViewItem; // Очистка панели DockPanel stack.Children.ClearO; // Повторное заполнение Filelnfo[] infos; try infos = item.Directoryinfo.GetFiles(); } catch return; } foreach (Fileinfo info in infos) { TextBlock text = new TextBlockO: text.Text = info.Name; stack.Chi 1dren.Add(text); } } Конструктор создает панель Grid с тремя столбцами — для DirectoryTreeView, для GridSplitter и для панели Scrollviewer, содержащей StackPanel. Здесь выводится информация о файлах. Конструктор назначает обработчик события SelectedltemChanged. Событие обрабатывается удалением всех дочерних элементов из панели StackPanel и ее повторным заполнением всеми файлами выделенного каталога. Конечно, сде- лать с этими файлами ничего нельзя, но мы прошли немалый путь к созданию клона Проводника Windows. Возможно, вы еще помните свойство ItemTemplate объекта ListBox. В главе 13 я показал, как определить объект DataTemplate, который задается свойству Item- Template. Объект DataTemplate включает свойство VisualTree, описывающее визу- альное представление дочерних объектов в ListBox.
TreeView и Listview 365 Свойство ItemTemplate, определяемое классом ItemsControl, наследуется TreeView. Более того, у DataTemplate имеется производный класс с именем HierarchicalData- Template, идеально подходящий для описания иерархических данных, из кото- рых строится дерево. Чтобы воспользоваться этим шаблоном, следует сначала определить класс с описанием иерархических данных, которые должны отобра- жаться в TreeView. Приведенный далее класс DiskDirectory по сути представляет собой «обертку» для объекта Directoryinfo. Более того, если бы класс Directoryinfo не был зафикси- рован (sealed), я бы объявил DiskDirectory производным от Directoryinfo, что не- много упростило бы класс. Например, можно было бы обойтись без конструкто- ра и свойства Name. DiskDirectory.cs //------------------------------------------------ // DiskDIrectory.cs (c) 2006 by Charles Petzold //------------------------------------------------ using System: using System.Collections.Generic; using System.10: namespace Petzold.TempiateTheTree { public class DiskDirectory { Di rectoryInfo dirinfo: // Конструктор получает объект Di rectory Info public DiskDirectory(Di rectoryinfo dirinfo) { this.dirinfo = dirinfo; // Свойство Name возвращает имя каталога public string Name get { return dirinfo.Name; } } // Свойство Subdirectories возвращает коллекцию // объектов DiskDirectory public List<DiskDirectory> Subdirectories { get { List<DiskDirectory> dirs = new List<DiskDirectory>(): Directorylnfo[] subdirs; try
366 Глава 16. TreeView и ListView { subdirs = dirinfo.GetDirector1es(); catch return dirs: } foreach (DirectoryInfo subdir in subdirs) dirs.Add(new DiskDirectory(subdir)); return dirs: } } } } Особенно важную роль играет свойство Subdirectories, которое вызывает ме- тод GetDirectories, создает для каждого подкаталога объект DiskDirectory и добав- ляет его в коллекцию List. Полученный объект List возвращается в качестве зна- чения свойства. Программа TemplateTheTree использует класс DiskDirectory при определении шаблона данных для TreeView. TemplateTheTree.cs //------------------------------------------------ // TemplateTheTree.cs (с) 2006 by Charles Petzold //------------------------------------------------ using System: using System.10: using System.Windows: using System.Windows.Controls: using System.Windows.Data: using System.Windows.Input: using System.Windows.Media: namespace Petzold.TempiateTheTree { public class TemplateTheTree : Window { [STAThread] public static void MainO Application app = new Application(); app.Run(new TempiateTheTree()); public TempiateTheTree() Title = "Template the Tree":
TreeView и ListView 367 // Создание объекта TreeView и его задание // в качестве содержимого окна TreeView treevue = new TreeView(); Content = treevue; // Создание объекта HierarchicalDataTemplate // на основе DiskDirectory HierarchicalDataTemplate template = new Hi erarchi calDataTemplate(typeof(DiskDirectory)); // Задание свойства Subdirectories в качестве источника данных template.ItemsSource = new Binding("Subdirectories"): // Создание объекта FrameworkElementFactory для TextBlock FrameworkElementFactory factoryTextBlock = new FrameworkElementFactory(typeof(TextBlock)); // Привязка свойства Text к свойству Name объекта DiskDirectory factoryTextBlock.SetBi ndi ng(TextBlock.TextProperty, new Binding(”Name”)); // Объект Textblock задается свойству VisualTree шаблона template.VisualTree = factoryTextBlock; // Создание объекта DiskDirectory для системного диска DiskDirectory dir = new DiskDirectory( new Directorylnfo( Path.GetPathRoot(Envi ronment.SystemDi rectory))); // Создание корневого объекта TreeViewItem // и задание его свойств TreeViewItem item = new TreeViewltem(); item.Header = dir.Name; item.ItemsSource = dir.Subdirectories; item.ItemTemplate = template; // Включение объекта TreeViewItem в TreeView treevue.Items.Add(item); item.IsExpanded = true; } } Созданный объект HierarchicalDataTemplate базируется на классе DiskDirectory. Шаблон связывается с объектом TreeViewItem. HierarchicalDataTemplate содержит свойство ItemsSource, определяющее источник данных для коллекции Items. Им является свойство Subdirectories объекта DiskDirectory.
368 Глава 16. TreeView и ListView На следующем шаге строится визуальное дерево, основанное на объектах FrameworkElementFactory. Для простоты в этом примере визуальное дерево Tree- ViewItem представлено простым объектом TextBlock. Свойство Text объекта TextBlock привязывается к свойству Name объекта DiskDirectory. Выполнение конструктора продолжается созданием объекта типа DiskDirectory для системного диска. (Я снова постарался упростить пример и не стал полу- чать информацию обо всех дисках.) Создается один объект TreeViewItem, у которо- го свойству Header задается свойство Name объекта DiskDirectory, свойству Items- Source — свойство Subdirectories объекта DiskDirectory, а свойству ItemTemplate — только что созданный объект HierarchicalDataTemplate. Таким образом, дочерние узлы корневого объекта TreeViewItem базируются на свойстве Subdirectories создан- ного объекта DiskDirectory, но для их отображения будет использоваться шаблон HierarchicalDataTemplate, который указывает, откуда берутся будущие узлы и как они должны отображаться. Элемент TreeView обеспечивает эффективное отобра- жение дерева каталогов без обработчиков событий. Программа демонстрирует использование описанной методики в ситуации, когда все узлы TreeViewItem представляют однотипные объекты. Тем не менее она также может использоваться и с иерархическими деревьями, имеющими разнотипные родительские и дочерние объекты. Дополнительные примеры ис- пользования шаблонов приводятся в главе 25. В последнем примере использования TreeView выводится список всех откры- тых классов WPF, производных от Dispatcherobject. Объект TreeView идеально подходит для этой роли, потому что визуальная иерархия узлов хорошо соот- ветствует иерархии наследования. Чтобы программа имела чуть более обобщен- ный характер, давайте напишем класс для получения имени, связанного с про- извольным объектом Туре. Класс TypeTreeViewItem, производный от TreeViewItem, включает свойство Туре. Разумеется, свойство Туре относится к типу Туре. TypeTreeViewItem.cs //------------------------------------------------- // TypeTreeViewItem.cs (с) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Windows; using System.Windows.Controls: namespace Petzold.ShowClassHierarchy { class TypeTreeViewItem : TreeViewItem { Type typ; // Два конструктора public TypeTreeViewItemO
TreeView и ListView 369 } public TypeTreeViewItem(Type typ) { Type = typ: // Открытое свойство Type типа Type public Type Type { set { typ = value: if (typ.IsAbstract) Header = typ.Name + " (abstract)": else Header = typ.Name: get { return typ; } } } } Программа содержит два конструктора для задания свойства Туре при созда- нии объекта или на более поздней стадии. Обратите внимание: метод доступа set свойства Туре задает свойству Header объекта TreeViewItem свойство Name объ- екта Туре, возможно с добавлением суффикса (abstract). Например, создание объ- екта типа TypeTreeViewItem и задание свойства Туре может выглядеть так: ТуpeTreeViewItem item = new TypeTreeViewItem(): item.Type = typeof(Button); В узле дерева будет выводиться простой текст «Button», однако объект Туре, связанный с этим узлом, по-прежнему будет доступен через свойство Туре. (Ме- тод доступа set свойства Туре может задать Header сам объект Туре, но в этом случае для вывода объекта Туре будет использоваться метод ToString класса Туре, который возвращает полностью уточненное имя — например, «System.Windows. Controls.Button».) На следующем этапе создания программы определяется класс ClassHierarchy- TreeView, производный от TreeView. ClassHierarchyTreeView.cs //------------------------------------------------------ // ClassHierarchyTreeView (с) 2006 by Charles Petzold //------------------------------------------------------ using System: using System.Col 1ections.Generic;
370 Глава 16. TreeView и ListView using System.Reflection; using System.Windows; using System.Windows.Controls; namespace Petzold.ShowClassHi erarchy { public class ClassHierarchyTreeView : TreeView { public ClassHierarchyTreeView(Type typeRoot) // Гарантированная загрузка Presentationcore UIEIement dummy = new UIElementO; // Список List для размещения сборок List<Assembly> assemblies = new List<Assembly>(); // Получение всех сборок, на которые имеются ссылки AssemblyNameE] anames = Assembly.GetExecutingAssembly().GetReferencedAssemblies(); // Включение в список assemblies foreach (AssemblyName aname in anames) assemblies.Add(Assembly.Load(aname)); // Сохранение потомков typeRoot в сортированном списке SortedList<string, Type> classes = new SortedList<string, Type>(); classes.Add(typeRoot.Name. typeRoot); // Получение всех типов в сборке foreach (Assembly assembly in assemblies) foreach (Type typ in assembly.GetTypes()) if (typ.IsPublic && typ.IsSubclassOf(typeRoot)) classes.Add(typ.Name, typ); // Создание корневого узла TypeTreeViewItem item = new TypeTreeViewItem(typeRoot); Iterns.Add(item); // Вызов рекурсивного метода CreateLinkedItems(item. classes); } void CreateLinkedItems(TypeTreeViewItem itemBase. SortedList<string. Type> list)
TreeView и ListView 371 —--------- foreach (KeyValuePair<string, Type> kvp in list) if (kvp.Value.BaseType == itemBase.Type) { TypeTreeViewItem item = new TypeTreeViewltem(kvp.Value); itemBase.Iterns.Add(item); CreateLinkedItems(item. list); } } Все происходит в конструкторе класса. Конструктору необходим корневой объект Туре, начинающий иерархию. Сначала он создает объект List для хране- ния всех объектов AssemblyName, ссылки на которые присутствуют в программе, а затем загружает эти сборки для создания объектов Assembly. Далее конструктор создает объект SortedList для хранения объектов Туре, представляющих классы, производные от typeRoot. Эти объекты Туре сортиру- ются по свойству Name. При поиске этих классов конструктор просто перебира- ет все объекты Assembly и объекты Туре в каждой сборке. После того как все классы будут собраны в SortedList, конструктор должен создать объект TypeTreeViewItem для каждого из этих классов. Эту работу выпол- няет рекурсивный метод CreateLinkedltems. Первый аргумент содержит объект TypeTreeViewItem, ассоциированный с конкретным классом. Метод просто пере- бирает все классы в SortedList и находит те из них, для которых этот класс явля - ется базовым типом. Метод создает для каждого класса новый объект Type- TreeViewItem и включает его в коллекцию базового класса. Два предыдущих и следующий файлы с исходным кодом являются частью проекта ShowClassHierarchy. Конструктор задает в качестве содержимого окна объект типа ClassHierarchyTreeView с корневым классом Dispatcherobject. ShowClassHierarchy.cs //--------------------------------------------------- // ShowClassHierarchy.cs (с) 2006 by Charles Petzold //--------------------------------------------------- using System: usi ng System.Col 1ecti ons.Generi c: using System.Reflection; using System.Windows; using System.Windows.Controls; namespace Petzold.ShowClassHierarchy { class ShowClassHierarchy : Window { [STAThread] public static void MainO {
372 Глава 16. TreeView и ListView Application app = new Appl1cat1on(): app.Run(new ShowClassH1erarchy()); } public ShowClassHieparchy() { Title = "Show Class Hierarchy": // Создание ClassHlerarchyTreeVlew ClassHlerarchyTreeVlew treevue = new ClassH1erarchyTreeV1ew( typeof(System.WIndows.Threading.DlspatcherObject)): Content = treevue; } } } Возможно, вам захочется заменить ссылку на Dispatcherobject на typeof(Object), чтобы получить еще большую иерархию классов, но при наличии двух одно- именных классов в разных пространствах имен логика ClassHierarchyTreeView пе- рестает работать — а при создании иерархии от Object дело обстоит именно так. Если элемент TreeView похож на левую панель Проводника Windows, элемент ListView напоминает правую панель этого приложения. В Проводнике Windows можно выбрать разные режимы отображения информации: значки, плитка, спи- сок, эскизы страниц пли таблица. В главе 13 уже было показано, как определить шаблон для ListBox и отображать объекты в разных форматах, поэтому для мно- гих режимов вывода объектов можно использовать ListBox с пользовательским шаблоном. Трудности возникают с видом Таблица (Details), в котором использу- ются столбцы с заголовками. Именно в таких ситуациях на помощь приходит элемент ListView. Элемент ListView является производным непосредственно от ListBox (a ListView- Item — от ListBoxItem). Как и ListBox, класс ListView содержит свойство Items для хранения объектов, отображаемых в элементе. ListView также содержит свойство ItemsSource, которому задается массив или другая коллекция объектов. Класс ListView добавляет к ListBox всего одно дополнительное свойство View типа View- Base. Если свойство View равно null, ListView фактически работает как ListBox. Пока у ViewBase существует только один производный класс GridView, ото- бражающий объекты в несколько столбцов с заголовками. Важнейшее свойство Gridview — Columns — является объектом типа GridViewColumnCollection. Для каж- дого отображаемого столбца следует создать объект типа GridViewColumn и вклю- чить его в коллекцию Columns. Объект GridViewColumn указывает заголовок столб- ца, ширину столбца, а также свойство объекта, которое должно отображаться в столбце. Предположим, коллекция Items элемента ListView заполнена объектами типа Personnel — класса со свойствами FirstName, LastName, EmailAddress и т. д. При опре- делении каждого столбца ListView вы указываете, какое свойство Personnel (First- Name, LastName и т. д.) должно отображаться в этом столбце. Свойства задаются с использованием привязок.
TreeView и ListView 373 Первая программа выводит все свойства и значения класса SystemParameters. Этот класс содержит многочисленные статические свойства — такие, как Menu- BarHeight (стандартная высота меню приложения) и IsMouseWheelPresent (нали- чие колеса у мыши). Первым шагом при отображении информации в GridView становится опреде- ление класса, описывающего отображаемые объекты. В нашей программе элемент GridView должен содержать два столбца: для имени и значения свойства. Класс для хранения объектов я назвал SystemParams. SystemParam.cs И---------------------------------------------- И SystemParam.cs (с) 2006 by Charles Petzold И namespace Petzold.LIstSystemParameters ( public class SystemParam { string strName; object objValue: public string Name { set { strName = value; } get { return strName; } } public object Value { set { objValue = value; } get { return objValue; } } public override string ToStringO { return Name + "=" + Value; } } } Класс ListSystemParameters, производный от Window, создает элемент ListView, заполняющий его клиентскую область. Затем создается объект GridView, который задается свойству View объекта ListView. ListSystemParameters.cs И------------------------------------------------------- И ListSystemParameters.cs (с) 2006 by Charles Petzold И------------------------------------------------------- using System; using System.Col lections.Generic; // Для экспериментов using System.ComponentModel; // Для экспериментов
374 Глава 16. TreeView и ListView using System.Reflection: using System.Windows; using System.Windows.Data; using System.Windows.Input: using System.Wi ndows.Control s; using System.Windows.Media: namespace Petzold.ListSystemParameters { class ListSystemParameters : Window [STAThread] public static void MainO { Application app = new ApplicationO: app.Run(new Li stSystemParameters()): } public ListSystemParameters0 { Title = "List System Parameters": // Создание объекта ListView как содержимого окна ListView Istvue = new ListViewO; Content = Istvue; // Создание объекта GridView // и задание его свойству View объекта ListView GridView grdvue = new GridViewO: Istvue.View = grdvue: // Создание двух столбцов GridView GridViewColumn col = new GridViewColumn(): col.Header = "Property Name": col.Width = 200: col.DisplayMemberBinding = new BindingC'Name"); grdvue.Columns.Add(col); col = new GridViewCol unin(); col.Header = "Value"; col.Width = 200: col.DisplayMemberBinding = new Binding("Value"): grdvue.Columns.Add(col); // Все параметры системы заносятся в один удобный массив Propertylnfo[] props = typeof(SystemParameters).GetProperties(): // Заполнение ListView foreach (Propertyinfo prop in props) if (prop.PropertyType != typeof(ResourceKey))
TreeView и ListView 375 { SystemParam sysparam = new SystemParamO; sysparam.Name = prop.Name; sysparam.Value = prop.GetValue(null, null); Istvue.Items.Add(sysparam); } } } Первый объект GridViewColumn предназначен для вывода имен свойств класса SystemParameters. Свойство Header содержит строку «Property Name», а ширина (Width) состав- ляет 200 аппаратно-независимых единиц. Свойство DisplayMemberBinding объекта GridViewColumn определяет привязку, которая обозначает отображаемые данные — в данном случае свойство Name объекта SystemParam: col .DisplayMemberBinding = new BindingC'Name"): Аналогично, у второго столбца свойству Header задается значение «Value», и он привязывается к свойству Value объекта SystemParams. До настоящего момента объект ListView не содержит ни одного элемента. Рабо- та конструктора завершается получением массива объектов Propertyinfo объекта SystemParameters с использованием рефлексии. Для каждого свойства программа создает объект типа SystemParam и задает его свойству Name значение свойства Name объекта Propertyinfo, а свойство Value задается методом GetValue объекта Propertyinfo. Все объекты включаются в коллекцию Items. Обратите внимание: все свойства SystemParameters типа ResourceKey игно- рируются. Каждое свойство класса существует в двух вариантах. Например, MenuBarHeight относится к типу double, a MenuBarHeightKey — к типу ResourceKey. Свойства типа ResourceKey в основном предназначены для использования в XAML (см. главу 21). Если удалить из программы команду Istvue.View = grdvue: то свойство View сохранит значение по умолчанию null, и элемент ListView будет вести себя как обычный список ListBox. Для отображения элементов использу- ются строки, возвращаемые методом ToString класса SystemParam; конечно, такая информация является полной, но выглядит не очень привлекательно. Один из недостатков программы ListSystemParameters состоит в том, что вы- водимые данные не отсортированы. Проблема может быть решена несколькими способами. Первый вариант — отсортировать массив Propertyinfo перед циклом foreach. Для этого нам потребуется небольшой класс, который реализует интер- фейс IComparer и содержит метод Compare для сравнения двух объектов Property- Info. Простая реализация такого класса может выглядеть так: class PropertylnfoCompare : IComparer<PropertyInfo>
376 Глава 16. TreeView и ListView public int Compare(PropertyInfo propl, Propertyinfo prop2) { return string.Compare(propl.Name, prop2.Name): } } Файл ListSystemParameters.es уже содержит директиву using для пространства имен System.Collections.Generic. Чтобы отсортировать массив Propertyinfo с исполь- зованием класса PropertylnfoCompare, достаточно следующего вызова: Array.Sort(props, new PropertylnfoCompareO); Второе решение требует директивы using для пространства имен System.Com- ponentModel (файл ListSystemParameters.es уже содержит ее). Вы создаете объект SortDescription с указанием свойства, по которому требуется провести сортиров- ку, и включаете его в коллекцию ItemCollection: 1stvue.Iterns.SortDescriptlons.Add( new SortDescriptionC'Name", ListSortDirection.Ascending)); Третье решение, использующее коллекцию SortedList, продемонстрировано в следующей версии программы. В нем определяется объект SortedList со строко- выми ключами и объектами SystemParam в качестве значений: SortedList<string, SystemParam> sortlist = new SortedList<string, SystemParam>(); Программа заполняет объект SortedList по содержимому Propertyinfo: foreach (Propertyinfo prop in props) if (prop.PropertyType != typeof(ResourceKey)) { SystemParam sysparam = new SystemParamO: sysparam.Name = prop.Name: sysparam.Value = prop.GetValue(null, null): sortlist.Add(prop.Name, sysparam): } Обратите внимание: свойство Name также определяет имя ключа SortedList, а соответственно, и порядок сортировки. Далее остается лишь задать свойству ItemsSource объекта ListView коллекцию Values объекта SortedList: 1stvue.ItemsSource = sortlist.Values: В свойстве Values объекта SortedList хранится отсортированная коллекция объектов SystemParam. Вторая версия программы также исправляет небольшую проблему со вто- рым столбцом. Поскольку в нем выводятся разнотипные данные, содержимое стоит выровнять по правому краю. Тем не менее вместо настройки выравнива- ния на уровне столбцов в программе используется более общее решение на базе шаблонов.
TreeView и ListView 377 Эта версия программы создает объект DataTemplate, инициализирует его ви- зуальным деревом с выравниванием объектов TextBlock по правому краю, после чего задает его свойству CellTemplate второго объекта GridViewColumn: ListSortedSystemParameters.cs и // ListSortedSystemParameters.cs (с) 2006 by Charles Petzold //- using Petzold.ListSystemParameters; // для SystemParam using System; using System.Collections.Generic; using System.Reflection: using System.Windows; using Systern.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; namespace Petzold.LIstSortedSystemParameters { public class LIstSortedSystemParameters : Window { [STAThread] public static void MainO { Application app = new ApplIcatlonO; app.Run(new LIstSortedSystemParameters()); } public LIstSortedSystemParameters() { Title = "List Sorted System Parameters"; // Создание объекта ListView как содержимого окна ListView Istvue = new ListViewO; Content = Istvue; // Создание объекта GridView и его задание // свойству View объекта ListView. GridView grdvue = new GridViewO; Istvue.View = grdvue; // Создание двух столбцов GridView GridViewColumn col = new GridViewColumnО; col.Header = "Property Name"; col.Width = 200; col.DisplayMemberBinding = new BindingC'Name"); grdvue.Columns.Add(col); col = new GrldViewColumnO:
378 Глава 16. TreeView и ListView col.Header = "Value": col.Width = 200: grdvue.Columns.Add(col); // Создание объекта DataTemplate для второго столбца DataTemplate template = new DataTemplate(typeof(str1ng)): FrameworkElementFactory factoryTextBlock = new FrameworkElementFactory(typeof(TextBlock)): factoryTextBlock.SetValue(TextBlock.HorizontalAl 1gnmentProperty. Horizontal AlIgnment.Right): factoryTextBlock.SetBIndlng(TextBl ock.TextProperty. new Blndlng("Value")): template.VIsualTree = factoryTextBlock: col.Cell Tempi ate = template: // Все параметры системы заносятся в один удобный массив Propertylnfotl props = typeof(SystemParameters).GetPropert1es(): // Создание объекта SortedLlst для хранения объектов SystemParam SortedLlst<strlng, SystemParam> sortlist = new SortedLlst<str1ng, SystemParam>(); // Заполнение SortedLlst по данным из массива Propertyinfo foreach (Propertyinfo prop In props) If (prop.PropertyType != typeof(ResourceKey)) SystemParam sysparam = new SystemParamO: sysparam.Name = prop.Name; sysparam.Value = prop.GetValue(nul1. null): sortlist.Add(prop.Name, sysparam): } // Задание свойства ItemsSource объекта ListView Istvue.ItemsSource = sort 11 st.Values; } } Обратите внимание: свойство Text объекта TextBlock привязано к свойству Value объекта SystemParam, поэтому обычный вариант с DisplayMemberBinding не- допустим. Во время экспериментов с зависимыми свойствами для главы 8 я написал небольшую консольную программу, которая использовала рефлексию для полу- чения информации о зависимых свойствах, определяемых некоторыми классами WPF. Но я хотел иметь более общее решение; так появились классы ClassHier- archyTreeView (см. ранее в этой главе) и DependencyPropertyListView (см. далее). Последняя программа этой главы ExploreDependencyProperties объединяет эти
TreeView и ListView 379 два элемента в одном окне, разделяя их вешкой разбивки. Если выбрать класс ClassHierarchyTreeView, элемент DependencyPropertyListView показывает зависимые свойства, определяемые этим классом, включая флаги FrameworkMetadata (если они есть). Класс DependencyPropertyListView является производным от ListView. Он пред- назначен для отображения коллекции объектов типа Dependencyproperty. Каждый столбец привязывается к некоторому свойству Dependency Property. Например, пер- вый столбец привязывается к свойству Name, а второй — к свойству OwnerType. Но здесь возникает проблема. Если просто привязать столбец ListView к свой- ству OwnerType объекта Dependencyproperty, будут выводиться неудобочитаемые строки вида «System.Windows.Controls.Button». Я хотел, чтобы в столбце выво- дилась простая строка «Button». К счастью, это можно сделать. При определе- нии объекта DataTemplate для свойства CellTemplate объекта GridViewColumn также определяется объект Binding, связывающий отображаемое свойство с элементом, который его отображает. В классе Binding определяется свойство Converter, кото- рое описывает преобразование привязанных данных в нужный формат. Оно от- носится к типу IValueConverter — это интерфейс, содержащий всего два метода Convert и ConvertBack. Итак, нам нужен класс, реализующий интерфейс IValueConverter, который пре- образует объект Туре (например, System.Windows.Controls.Button) в строку («Button»). TypeToString.cs и II TypeToString.cs (с) 2006 by Charles Petzold И using System: using System.Globalization: using System.Windows.Data: namespace Petzol d.ExploreDependencyProperti es { class TypeToString : IValueConverter { public object Convert(object obj, Type type, object param. Cultureinfo culture) { // Имя класса без пространства имен return (obj as Name).Type: } public object ConvertBack(object obj, Type type, object param. Cultureinfo culture) return null: } }
380 Глава 16. TreeView и ListView Преобразуемый объект передается методу Convert в первом параметре. Он пре- образуется в объект Туре, после чего свойство Name предоставляет имя без ука- зания пространства имен. В моем приложении метод ConvertBack не использует- ся, поэтому он просто возвращает null. Преобразование также хорошо подходит для отображения свойства PropertyType объекта Dependencyproperty. Как вы, вероятно, помните, Dependencyproperty определяет свойство Default- Metadata типа PropertyMetadata. Объект PropertyMetadata содержит кое-какую полез- ную информацию, особенно свойство Defaultvalue. Тем не менее многие элементы используют для определения своих метаданных объект типа FrameworkProperty- Metadata, содержащий важнейшие свойства AffectsMeasure, AffectsArrange, Affects- Render и др. Эти свойства метаданных часто задаются конструктором с исполь- зованием перечисления FrameworkPropertyMetadataOptions; именно на этом варианте я сосредоточил свои усилия. Мне не хотелось выделять целую группу столбцов под логические значения AffectsMeasure, AffectsArrange и т. д. Я решил ограничиться одним столбцом, в кото- ром бы выводились все члены FrameworkPropertyMetadataOptions, использовавшие- ся для исходного создания метаданных. Эту задачу и решает класс преобразова- ния: метод Convert преобразует объект типа FrameworkPropertyMetadata в объект типа FrameworkPropertyMetadataOptions. MetadataToFlags.cs // // MetadataToFlags.cs (с) 2006 by Charles Petzold // using System; using System.Globalization; using System.Windows; using System.Windows.Data; namespace Petzold.ExploreDependencyProperties { class MetadataToFlags : IValueConverter { public object Convert(object obj, Type type, object param, Cultureinfo culture) { FrameworkPropertyMetadataOptions flags = 0: FrameworkPropertyMetadata metadata = obj as FrameworkPropertyMetadata; if (metadata == null) return null; if (metadata.AffectsMeasure) flags |= FrameworkPropertyMetadataOptions.AffectsMeasure;
TrpeView и ListView 381 if (metadata.AffectsArrange) flags |= FrameworkPropertyMetadataOptions. AffectsArrange; if (metadata.AffectsParentMeasure) flags |= FrameworkPropertyMetadataOptions. AffectsParentMeasure; if (metadata.AffectsParentArrange) flags |= FrameworkPropertyMetadataOptions. AffectsParentArrange; if (metadata.AffectsRender) flags |= FrameworkPropertyMetadataOptions.AffectsRender; if (metadata.Inherits) flags |= FrameworkPropertyMetadataOptions.Inherits; if (metadata.OverridesInlieritanceBehavior) flags |= FrameworkPropertyMetadataOptions. Overri des Inheri tanceBehavi or; if (metadata.IsNotDataBindable) flags |= FrameworkPropertyMetadataOptions.NotDataBindable; if (metadata.BindsTwoWayByDefault) flags |= FrameworkPropertyMetadataOptions. Bi ndsTwoWayByDefault; if (metadata.Journal) flags |= FrameworkPropertyMetadataOptions.Journal; return flags; } public object ConvertBack(object obj. Type type, object param, Cultureinfo culture) { return new FrameworkPropertyMetadata(null. (FrameworkPropertyMetadataOptions)obj); } } } При отображении преобразованного объекта метод ToString, определяемый структурой Enum, форматирует перечисление в удобочитаемый список, разделен- ный запятыми. Настало время рассмотреть класс DependencyPropertyListView. Класс являет- ся производным от ListView и определяет свойство с именем Туре, поддерживае- мое зависимым свойством TypeProperty. Задайте свойство Туре объекта ListView,
382_____________________________________ Глава 16. TreeView и ListView _______,— и элемент выведет перечень всех объектов Dependencyproperty, определяемых указанным типом. Метод OnTypePropertyChanged получает все объекты Dependency- Property посредством рефлексии и сохраняет их в коллекции SortedLlst, исполь- зуемой для задания свойства ItemsSource элемента ListView. DependencyPropertyListView.cs // // DependencyPropertyListView.cs (с) 2006 by Charles Petzold // using System; using System.Collections.Generic; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Data; namespace Petzold.ExploreDependencyProperti es { public class DependencyPropertyListView : ListView { // Определение зависимого свойства для Type, public static Dependencyproperty TypeProperty; // Регистрация зависимого свойства в статическом конструкторе static DependencyPropertyListView() { TypeProperty = DependencyProperty.RegisterCType". typeof(Type). typeof(DependencyPropertyListV1ew), new PropertyMetadata(null, new PropertyChangedCal1 back(OnTypePropertyChanged))): } // Статический метод, вызываемый при изменении TypeProperty static void OnTypePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { // Получение используемого объекта ListView DependencyPropertyListView Istvue = obj as DependencyPropertyListView; // Получение нового значения свойства Type Type type = args.NewValue as Type; // Очистка всего текущего содержимого ListView Istvue.ItemsSource = null; // Получение всех полей Dependencyproperty объекта Type if (type != null)
383 frpeView и ListView___________________________________________________ { SortedList<string. DependencyProperty> list = new SortedList<string, DependencyProperty>(): Fieldlnfof] infos = type.GetFields(); foreach (Fieldinfo info in infos) if (info.FieldType == typeof(Dependencyproperty)) list.Adddnfo.Name, (Dependencyproperty)i nfo.GetValue(null)): // Список задается свойству ItemsSource Istvue.ItemsSource = list.Values: } } // Открытое свойство Type public Type Type { set { SetValue(TypeProperty, value); } get { return (Type)GetValue(TypeProperty): } } // Конструктор public DependencyPropertyListView() { // Создание объекта GridView // и его задание свойству View GridView grdvue = new GridViewO; this.View = grdvue: // В первом столбце отображается свойство 'Name' // объектов Dependencyproperty GridViewColumn col = new GridViewColumnO: col.Header = "Name": col.Width = 150: col.DisplayMemberBinding = new BindingC'Name"): grdvue.Columns.Add(col); // Второй столбец снабжается заголовком 'Owner' col = new GridViewColumnO: col.Header = "Owner"; col.Width = 100; grdvue.Columns.Add(col): // Во втором столбце отображается свойство 'OwnerType' // объектов Dependencyproperty // Для этого потребуется шаблон данных DataTemplate template = new DataTemplate(): col.Cell Tempi ate = template;
384 Глава 16. TreeView и ListView // Объект TextBlock для отображения данных FrameworkElementFactory elTextBlock = new FrameworkElementFactory(typeof(TextBlock)); template.VisualTree = elTextBlock; // Привязка свойства 'OwnerType' объектов Dependencyproperty // к свойству Text объекта TextBlock // с использованием преобразования TypeToString Binding bind = new Binding("OwnerType"): bi nd.Converter = new TypeToStringf): elTextBlock.SetBindlng(TextBlock.TextProperty. bind): // Третий столбец снабжается заголовком 'Type' col = new GridViewColumn(); col.Header = "Type": col.Width = 100; grdvue.Columns.Add(col); // Для него необходим аналогичный шаблон // для привязки к свойству 'PropertyType' template = new DataTemplate(): col.Cell Tempi ate = template: elTextBlock = new FrameworkElementFactory(typeof(TextBlock)); template.VisualTree = elTextBlock: bind = new BindingCPropertyType"); bi nd.Converter = new TypeToStringO; elTextBlock.SetBinding(TextBlock.TextProperty, bind): // В четвертом столбце с заголовком 'Default' // отображается DefaultMetadata.Defaultvalue col = new GridViewColumnO: col.Header = "Default"; col.Width = 75: col.DisplayMemberBinding = new Binding("DefaultMetadata.DefaultValue"); grdvue.Columns.Add(col); // С пятым столбцом аналогично col = new GridViewColumnO: col.Header = "Read-Only": col.Width = 75: col.DisplayMemberBinding = new Bi ndi ng("DefaultMetadata.Readonly"): grdvue.Columns.Add(col); // С шестым столбцом аналогично col = new GridViewColumnO;
TreeView и ListView 385 col.Header = "Usage": col.Width = 75; col.DisplayMemberBinding = new Binding("DefaultMetadata.AttachedPropertyUsage"); grdvue.Columns.Add(col); // В седьмом столбце отображаются флаги метаданных col = new GridViewColumnO; col.Header = "Flags"; col.Width = 250; grdvue.Columns.Add(col); // Необходим шаблон для преобразования // с использованием MetadataToFlags template = new DataTemplateO; col.Cell Tempi ate = template; elTextBlock = new FrameworkElementFactory(typeof(TextBlock)); template.VisualTree = elTextBlock; bind = new BindingU'DefaultMetadata"); bi nd. Converter = new MetadataToFlagsO ; elTextBlock.SetBinding(TextBlock.TextProperty, bind); } } } За определение столбцов отвечает в основном конструктор класса. Одни столбцы определяются элементарно; для других используются преобразования, о которых говорилось выше. Наконец, класс ExploreDependencyProperties объединяет все компоненты. Он создает объекты ClassHierarchyTreeView и DependencyPropertyListView, разделенные объектом GridSplitter. ExploreDependencyProperties.cs И И ExploreDependencyProperties.cs (с) 2006 by Charles Petzold И using Petzold.ShowClassHierarchy; // для ClassHierarchyTreeView using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ExploreDependencyProperti es public class ExploreDependencyProperties : Window
386 Глава 16. TreeView и ListView t [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new ExploreDependencyProperti es()); } public ExploreDependencyProperties() { Title = "Explore Dependency Properties"; // Создание объекта Grid как содержимого окна Grid grid = new GridO; Content = grid; // Определения трех столбцов для Grid ColumnDefinition col = new ColumnDefinition(); col.Width = new GridLengthd, GridUnitType.Star); grid.ColumnDefinitions.Add(new ColumnDefinition()); col = new ColumnDefinitionO; col.Width = GridLength.Auto; grid.ColumnDefinitions.Add(col): col = new ColumnDefinitionO; col.Width = new GridLength(3, GridUnitType.Star); grid.ColumnDefinitions.Add(col); // Слева находится элемент ClassHierarchyTreeView ClassHierarchyTreeView treevue = new ClassHierarchyTreeView(typeof(DependencyObject)); gri d.Chi 1dren.Add(treevue); Grid.SetColumn(treevue, 0); // В центральной ячейке размещается GridSplitter GridSplitter split = new GridSplitterO; split.HorizontalAlignment = HorizontalAlignment.Center; split.VerticalAlignment = VerticalAlignment.Stretch; split.Width = 6; grid.Children.Add(split); Grid.SetColumn(split, 1); // Справа находится элемент DependencyPropertyListView DependencyPropertyListView 1stvue = new DependencyPropertyLi stVi ew(); gri d.Chi 1dren.Add(1stvue); Grid. SetColumnO stvue, 2); // Настройка привязки между TreeView и ListView 1stvue.SetBi ndi ng(DependencyPropertyLi stVi ew.TypeProperty,
TreeView и ListView 387 —- —--------- "Selectedltern.Type"): Istvue.DataContext = treevue; } } } Две последние команды конструктора задают привязку между свойством Туре элемента DependencyPropertyListView и свойством Туре свойства Selectedltem элемента ClassHierarchyTreeView. Простота привязки оправдывает (или почти оп- равдывает) всю проделанную работу.
Печать и диалоговые # окна Если вам захочется острых ощущений, попробуйте просмотреть пространство имен System.Printing. Вы найдете в нем множество классов, относящихся к драй- верам принтеров, очередям, серверам печати и заданиям печати. Впрочем, не все так страшно: в большинстве приложений будет использоваться лишь малая часть содержимого System.Printing. Вероятно, основная логика печати в ваших прило- жениях будет базироваться на классе PrintDialog, определяемом в пространстве имен System.Windows.Controls. Из всех классов System.Printing вам прежде всего понадобится PrintTicket. Кро- ме того, для работы с этим классом в проект должна быть включена ссылка на сборку ReachFramework.dll. В программах с поддержкой печати также полезно поддерживать поле типа PrintQueue. Этот класс тоже находится в пространст- ве имен System.Printing, но происходит из сборки System.Printing.dll. Другие клас- сы, относящиеся к печати, определяются в пространстве имен System.Windows. Documents. Естественно, класс PrintDialog прежде всего предназначен для вывода диало- гового окна печати, но он также содержит методы для печати отдельной страни- цы или многостраничного документа. В обоих случаях изображение, выводимое на печатной странице, представлено объектом типа Visual. Как вам уже известно, среди важных классов, производных от Visual, присутствует класс UIEIement; это означает, что вывести на печать можно экземпляр любого класса, производного от FrameworkElement, включая панели, элементы управления и другие интерфейс- ные элементы. Например, можно создать объект Canvas или другую панель, раз- местить на нем дочерние элементы или геометрические фигуры, а затем вывес- ти его на печать. Хотя печать панели со всем содержимым открывает достаточно широкие воз- можности, существует другое, более прямолинейное решение — воспользоваться классом DrawingVisual, который тоже является производным от Visual. Применение DrawingVisual было продемонстрировано в классе ColorCell из проекта SelectColor главы И. Класс DrawingVisual содержит метод RenderOpen, который возвращает объект типа Drawingcontext. Вызовы методов Drawingcontext (завершающиеся вы- зовом Close) сохраняют графику в объекте DrawingVisual. Вероятно, приведенная далее программа PrintEllipse демонстрирует самый простой пример печати из всех возможных. Для запуска печати в ней использу-
ПемаТь и диалоговые окна 389 ется обычная кнопка. Если щелкнуть на кнопке, программа создает объект типа PrintDialog и выводит его на экран. В диалоговом окне можно выбрать принтер (если в системе установлено несколько принтеров), и, возможно, изменить не- которые настройки печати. Затем кнопка запуска печати закрывает PrintDialog, и программа начинает подготовку к печати. PrintEllipse.cs // // PrintEllipse.cs (с) 2006 by Charles Petzold И using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.PrintEllipse { public class PrintEllipse ; Window { [STAThread] public static void MainO { Application app = new Application(); app.Run(new PrintEllipseO): } public PrintEllipseO Title = "Print Ellipse"; FontSize = 24; // Создание объекта StackPanel как содержимого окна StackPanel stack = new StackPanel О; Content = stack; // Создание кнопки, инициирующей процесс печати Button btn = new ButtonO; btn.Content = "_Print... btn.HorizontalAlignment = HorizontalAlignment.Center; btn.Margin = new Thickness(24); btn.Click += PrintOnClick; stack. ChiIdren.Add(btn); } void PrintOnClick(object sender, RoutedEventArgs args) { PrintDialog dig = new PrintDialogO; i f ((bool)dlg.ShowDi a 1og().GetVa1ueOrDefault())
390 Глава 17. Печать и диалоговые окна // Создание объекта DrawingVisual // и открытие графического контекста (DrawingContext) DrawingVisual vis = new DrawingVisualО; DrawingContext de = vis.RenderOpenO: // Вывод эллипса dc.DrawEllipse(Brushes.LightGray, new Pen(Brushes.Black, 3), new Point(dlg.PrintableAreaWidth / 2, dlg.PrintableAreaHeight / 2). dig.PrintableAreaWidth / 2, dlg.PrintableAreaHeight / 2); // Закрытие графического контекста dc.CloseO: // И наконец, печать страницы dlg.PrintVisual(vis, "Му first print job"); } } } } Согласно определению, метод ShowDialog возвращает bool с возможным null. Для этого конкретного диалогового окна ShowDialog возвращает true, если поль- зователь щелкнул на кнопке печати; false, если пользователь щелкнул на кнопке отмены; или null, если диалоговое окно было закрыто красной кнопкой у право- го края заголовка. Вызов GetValueOrDefault преобразует null в false, чтобы резуль- тат можно было безопасно проверить в команде if. Если метод ShowDialog возвращает true, программа создает объект типа Draw- ingVisual, вызывает RenderOpen для получения объекта DrawingContext, после чего вызывает DrawEllipse и Close для DrawingContext. Наконец, объект DrawingVisual пе- редается методу PrintVisual окна PrintDialog с текстовой строкой, идентифицирую- щей задание печати в очереди принтера. В аргументах DrawEllipse используются два свойства, определяемых классом PrintDialog: PrintableAreaWidth и PrintableAreaHeight. Названия этих свойств выбраны не совсем удачно. Когда страница, выведенная программой PrintEllipse, будет напечатана, некоторые части эллипса могут быть усечены, потому что они на- ходятся за границами печатной области страницы. Свойства PrintableAreaWidth и PrintableAreaHeight обозначают не печатную область, а задают общий физиче- ский размер страницы в аппаратно-независимых единицах (1/96 дюйма). У нижнего края окна Print расположены четыре переключателя, при помощи которых выбирается печатаемая часть документа: весь документ, текущее выде- ление, текущая страница или диапазон страниц, заданный пользователем. Все переключатели, кроме первого, по умолчанию заблокированы. Первая версия WPF не позволяла снять блокировку с переключателей выделенного фрагмента и те- кущей страницы. Чтобы сделать доступным режим диапазонной печати, следо- вало задать свойству UserPageRangeEnabled значение true. Для инициализации переключателей и последующего получения пользова- тельского решения используется свойство PageRangeSelection, принимающее зна- чения AllPages или UserPages из одноименного перечисления. Если свойство равно
рриать и диалоговые окна 391 pageRangeSelection.UserPages (соответствует переключателю Pages), свойство Раде- Ranges объекта PrintDialog содержит коллекцию объектов типа PageRange (струк- тура со свойствами PageFrom и РадеТо). В диалоговом окне печати также имеется поле для ввода количества копий. Если ввести в нем число, большее 1, метод PrintVisual напечатает несколько ко- пий страницы. Диалоговое окно печати также содержит кнопку вызова свойств принтера. Если выбрать в свойствах другой размер страницы, по результатам печати становится очевидно, что он отражается в свойствах PrintableAreaWidth и PrintableAreaHeight. Здесь также можно переключить ориентацию страницы, но в данной программе эффект не столь заметен. Данные, запрашиваемые пользователем (ориентация, количество копий и т. д.), сохраняются в свойстве PrintTicket объекта PrintDialog. Свойство относится к типу PrintTicket — этот класс определяется в пространстве имен System.Printing и хра- нится в сборке Reach Framework. Для использования PrintTicket в программу необ- ходимо включить ссылку на ReachFramework.dll. Если активизировать PrintDialog в программе PrintEllipse, изменить параметр (например, ориентацию страницы), выполнить печать, а затем снова активизи- ровать PrintDialog, измененный параметр возвращается к значению по умолча- нию. В наши дни «вежливые» программы обычно сохраняют пользовательские настройки между сеансами печати. Для этого в программе определяется поле типа PrintTicket. PrintTicket prntkt: Конечно, в исходном состоянии значение prntkt равно null. При создании объ- екта типа PrintDialog его свойство PrintTicket инициализируется значением этого поля, но только в том случае, если оно отлично от null: PrintDialog dig = new PrintDialog(): if (prntkt != null) dig.PrintTicket = prntkt: Когда пользователь щелкает на кнопке печати в диалоговом окне, текущий набор настроек печати сохраняется в этом поле: if (dig.ShowDialog().GetValueOrDefault()) { prntkt = dig.PrintTicket: } Чтение и запись PrintTicket обеспечивает восстановление пользовательских настроек. Если пользователь откроет окно печати, выберет альбомную ориента- цию страницы, щелкнет на кнопке ОК, а затем снова откроет диалоговое окно печати, в нем будет выбрана альбомная ориентация. Если на компьютере установлено несколько принтеров, они будут перечис- лены в поле ComboBox в верхней части окна печати. Если пользователь выбирает в окне печати принтер, отличный от принтера по умолчанию, вероятно, этот же
392 Глава 17. Печать и диалоговые окна принтер следует выбрать при следующем вызове окна. Для этого можно сохра- нить свойство PrintQueue диалогового окна в поле типа PrintQueue и задать свойст- во по значению поля при следующем вызове окна. Для использования PrintQueue в программу необходимо включить ссылку на сборку System.Printing. Одним из аспектов печати, которые невозможно выбрать в PrintDialog, является настройка полей страницы. Давайте создадим диалоговое окно, предназначенное специально для этой цели. Классы диалоговых окон наследуют от Window, и они очень похожи на другие классы на базе Window. Впрочем, существуют и некото- рые отличия. Конструктор класса диалогового окна должен задать свойство ShowInTaskbar равным false. Очень часто размеры диалогового окна подбираются по содержи- мому, а свойство ResizeMode задается равным NoResize. Вы можете задать свойст- во WindowStyle равным SingleBorderWindow или ToolWindow, по своему усмотрению. Отображать диалоговые окна без заголовка сейчас считается неправильным. В простейшем варианте позиционирования диалогового окна относительно владельца свойству WindowStartupLocation задается значение CenterOwner. Даже если оставить свойству WindowStartupLocation значение по умолчанию (при кото- ром система Windows сама выбирает позицию окна), свойству Owner диалогово- го окна все равно должен быть задан объект Window, вызвавший окно. Обычно код выглядит примерно так: MyDialog dig = new MyDialogO: dig.Owner = this: Если на экране отображается диалоговое окно с владельцем null, а пользова- тель переключается на другое приложение, диалоговое окно может оказаться позади окна приложения, из которого оно было вызвано, но окно приложения все равно не сможет получать ввод от пользователя. Ситуация не пз приятных. Класс CommonDialog из пространства имен Microsoft.Win32 (базовый для Ореп- FileDialog и SaveFileDialog) определяет метод ShowDialog с аргументом Owner. Это один из способов. Также аргумент Owner может передаваться конструктору клас- са диалогового окна. Выбор этого варианта предоставляет чуть большую гибкость при позиционировании диалогового окна. Например, если потребуется сместить диалоговое окно на один дюйм от левого верхнего угла окна приложения, доста- точно включить в конструктор совсем простой код: public MyDialog(Window owner) { Left = 96 + owner.Left; Top = 96 + owner.Top: Диалоговое окно почти всегда определяет по крайней мере одно открытое свойство для информации, которую окно получает от пользователя и предостав- ляет приложению. Можно даже определить класс или структуру для хранения всей информации, предоставляемой конкретным диалоговым окном. В опреде-
Поиятьидиалоговые окна 393 дении этого свойства метод доступа set инициализирует элементы окна по значе- ниям свойства. Метод доступа get получает значения свойств из элементов. Диалоговое окно содержит кнопки ОК и Cancel, хотя кнопка ОК может назы- ваться Open, Save, Print или как-нибудь еще. У кнопки ОК свойство IsDefault зада- но равным true, так что нажатие клавиши Enter в диалоговом окне активизирует эту кнопку. У кнопки Cancel истинно свойство IsCancel, поэтому кнопка срабаты- вает при нажатии клавиши Escape. Предоставлять обработчик Click для кнопки Cancel не обязательно. Диалого- вое окно уничтожается автоматически в результате действия истинного свойст- ва IsCancel. От обработчика Click кнопки ОК требуется лишь задать истинное зна- чение свойству DialogResult. Диалоговое окно уничтожается, а метод ShowDialog возвращает t« ue. Класс PageMarginsDialog содержит четыре элемента TextBox, в которых пользо- ватель вводит величину левого, правого, верхнего и нижнего полей (в дюймах). Класс определяет одно открытое свойство PageMargins типа Thickness, осуществ- ляющее преобразование между дюймами и аппаратно-независимыми единица- ми. Метод доступа get свойства PageMargins может без всяких опасений вызвать Double.Parse для преобразования содержимого четырех элементов TextBox в чис- ла, так как класс блокирует кнопку ОК, пока все поля не будут содержать дейст- вительные значения. PageMarginsDialog.cs и II PageMarginsDialog.cs (с) 2006 by Charles Petzold и using System: using System.Windows; using System.Windows.Controls: using System.Windows.Controls.Primitives; namespace Petzold.PrintWithMargins { class PageMarginsDialog : Window { // Внутреннее перечисление для идентификации сторон страницы enum Side Left. Right. Top. Bottom } // Четыре элемента TextBox для ввода числовых данных TextBox[] txtbox = new TextBox[4]; Button btnOk: // Открытое свойство типа Thickness // для хранения полей страницы
394 Глава 17. Печать и диалоговые окна public Thickness PageMarglns { set { txtbox[(1nt)Side.Left].Text = (value.Left / 96).ToString("F3"); txtbox[(1nt)Side.Right].Text = (value.Right / 96).ToString("F3"); txtbox[(1nt)Side.Top].Text = (value.Top / 96).ToString("F3"); txtbox[(int)Side.Bottom].Text = (value.Bottom / 96).ToString("F3"); } get { return new Thickness( Double.Parse(txtbox[(1nt)Side.Left].Text) * 96. Double.Parse(txtbox[(int)Side.Top],Text) * 96, Double.Parse(txtbox[(int)Side.Right].Text) * 96. Double.Parse(txtbox[(int)Side.Bottom].Text) * 96); } } // Конструктор public PageMarginsDialog() { // Стандартные настройки для диалоговых окон Title = "Page Setup"; ShowInTaskbar = false; WindowStyle = WindowStyle.Tool Window; WindowStartupLocation = WindowStartupLocation.CenterOwner; SizeToContent = SizeToContent.WldthAndHeight; ResizeMode = ResizeMode.NoResize; // Создание объекта StackPanel // и его назначение содержимым окна StackPanel stack = new StackPanelО; Content = stack; // Создание GroupBox как дочернего объекта StackPanel GroupBox grpbox = new GroupBoxO; grpbox.Header = "Margins (inches)"; grpbox.Margin = new Thickness(12); stack.Chi 1dren.Add(grpbox); // Назначение объекта Grid содержимым GroupBox Grid grid = new GridO; grid.Margin = new Thickness(6);
Пемать и диалоговые окна 395 grpbox.Content = grid; // Две строки и четыре столбца for (int 1=0; 1 < 2; 1++) { RowDef1nition rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; grid.RowDefinit1ons.Add(rowdef); } for (int i = 0; i < 4; 1++) { ColumnDefinition coldef = new ColumnDefinitionO; coldef.Width = GridLength.Auto: grid.ColumnDefinitions.Add(coldef); // Размещение элементов Label и TextBox в элементе Grid for (int i = 0; i < 4; 1++) { Label Ibl = new LabelO; Ibl.Content = + Enum.GetName(typeof(S1de). 1) + "; Ibl.Margin = new Th1ckness(6); Ibl.VerticalAlignment = VerticalAlignment.Center; grid.Chi Idren.Adddbl); Grld.SetRowdbl, 1/2); Grid.SetColumn(Ibl, 2 * (1 % 2)); txtbox[1 ] = new TextBoxO; txtbox[1].TextChanged += TextBoxOnTextChanged; txtbox[1].M1nW1dth = 48; txtbox[1].Margin = new Th1ckness(6); gr1d.Chi 1dren.Add(txtbox[1]): Grid.SetRow(txtbox[i], 1/2); Gr1d.SetColumn(txtbox[1], 2 * (1 % 2) + 1); } // Создание элемента UniformGrid для кнопок OK и Cancel UniformGrid unigrid = new UniformGrid(); unigrid.Rows = 1; unigrid.Columns = 2; stack.ChiIdren.Add(unlgrld); btnOk = new ButtonO; btnOk.Content = "OK"; btnOk.IsDefault = true; btnOk.IsEnabled = false; btnOk.MlnWldth = 60; btnOk.Margin = new Thickness(12); btnOk.HorizontalAlIgnment = HorizontalAlignment.Center;
396 Глава 17. Печать и диалоговые окна btnOk.Click += OkButtonOnClick; unigrid.Children.Add(btnOk); Button btnCancel = new ButtonO; btnCancel.Content = "Cancel"; btnCancel.IsCancel = true; btnCancel.MinWidth = 60: btnCancel.Margin = new Th1ckness(12); btnCancel.HorizontalAlignment = HorizontalAlignment.Center; unigrid.Children.Add(btnCancel); } // Кнопка OK доступна только в том случае, если элементы TextBox // содержат числовые данные void TextBoxOnTextChanged(object sender, TextChangedEventArgs args) { double result: btnOk.IsEnabled = Double.TryParse(txtbox[(1nt)S1de.Left].Text, out result) && Double.TryParse(txtbox[(1nt)S1de.Right].Text, out result) && Double.TryParse(txtbox[(1nt)S1de.Top].Text, out result) && Double.TryParse(txtbox[(1nt)S1de.Bottom].Text, out result): } // Кнопка OK убирает диалоговое окно с экрана void OkButtonOnCl1ck(object sender, RoutedEventArgs args) { DlalogResult = true; } } } Класс PageMarginsDialog входит в проект PrintWithMargins, из которого также позаимствован приведенный файл. Программа демонстрирует использование объектов PrintQueue и PrintTicket для сохранения и загрузки параметров между вы- зовами диалогового окна. Окно PageMarginsDialog вызывается кнопкой Page Setup. PrintWithMargins.cs // // Pr1ntW1thMarg1ns.cs (с) 2006 by Charles Petzold // using System: using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Windows.Input: using System.Windows.Media: using System.Printing;
Печать и диалоговые окна 397 namespace Petzold.Pri ntWi thMargins { public class PrintWithMargins : Window { // Приватные поля для сохранения информации из PrintDialog PrintQueue prnqueue; PrintTicket prntkt: Thickness marginPage = new Thickness(96): [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new PrintWithMargins О); } public PrintWithMarginsO { Title = "Print with Margins": FontSize = 24: // Создание объекта StackPanel как содержимого окна StackPanel stack = new StackPanel(); Content - stack; // Создание кнопки Page Setup Button btn = new ButtonO; btn.Content = "Page Set_up...": btn.Horizontal Alignment = HorizontalAlignment.Center; btn.Margin = new Thickness(24); btn.Click += SetupOnClick; stack.Chi 1dren.Add(btn); // Создание кнопки Print btn = new ButtonO: btn.Content = "_Print...": btn.HorizontalAlignment = HorizontalAlignment.Center: btn.Margin = new Thickness(24); btn.Click += PrintOnClick: stack.Children.Add(btn); } // Кнопка Page Setup; вызов окна PageMarginsDialog void SetupOnClick(object sender. RoutedEventArgs args) { // Создание диалогового окна и инициализация PageMarglns PageMarginsDialog dig = new PageMarginsDialogO: dig.Owner = this;
398 Глава 17. Печать и диалоговые окна dig.PageMargins = marginPage: If (dig.ShowDIa 1og().GetVa1ueOrDefault()) { // Сохранение полей страницы из диалогового окна. marginPage = dig.PageMargins: } } // Кнопка Print: вызов окна PrintDialog void PrintOnClick(object sender. RoutedEventArgs args) { PrintDialog dig = new PrintDialogO: // Инициализация PrintQueue и PrintTicket по данным полей if (prnqueue != null) dig.PrintQueue = prnqueue; If (prntkt != null) dig.PrintTicket = prntkt; If (dig.ShowDIa1og().GetVa1ueOrDefaul10) { // Сохранение PrintQueue и PrintTicket из диалогового окна prnqueue = dig.PrintQueue; prntkt = dig.PrintTicket; // Создание объекта DrawingVisual и открытие DrawingContext DrawingVisual vis = new DrawingVisual(); DrawingContext de = vis.RenderOpenO; Pen pn = new Pen(Brushes.Black, 1); // Прямоугольник описывает размеры страницы за вычетом полей Rect rectPage = new Rect(marginPage.Left. marginPage.Top, dig.PrintableAreaWidth - (marginPage.Left + marginPage.Right), dlg.PrintableAreaHeight - (marginPage.Top + marginPage.Bottom)); // Рисование прямоугольника, представляющего границы полей dc.DrawRectangle(null. pn, rectPage); // Создание форматированного текста /7 со свойствами PrlntableArea FormattedText formtxt = new FormattedText( String.FormatC'Hello, Printer! {0} x {1}", dig.PrintableAreaWidth / 96. dlg.PrintableAreaHeight / 96), Cuiturelnfo.CurrentCulture. FlowDIrect1 on.LeftToRi ght. new Typeface(new FontFamily("Times New Roman"),
ррцать и диалоговые окна 399 Fontstyles.Italic, FontWeights.Normal, FontStretches.Normal), 48, Brushes.Bl ack); // Получение физических размеров отформатированной строки Size sIzeText = new Size(formtxt.Width, formtxt.Height); // Вычисление точки для центровки текста в границах полей Point ptText = new Point(rectPage.Left + (rectPage.Width - formtxt.Width) / 2, rectPage.Top + (rectPage.Height - formtxt.Height) / 2); // Вывод текста и окружающего прямоугольника de.DrawText(formtxt, ptText); dc.DrawRectangle(null, pn, new Rect(ptText, sIzeText)); // Закрытие DrawingContext dc.CloseO; // Вывод страниц(ы) dlg.PrintVisual(vis, Title); } } } Программа печатает прямоугольник, размеры которого определяются поля- ми страницы, и выводит в его центре текст, также заключенный в прямоуголь- ник. Текст состоит из слов «Hello, Printer!», за которыми следуют размеры стра- ницы в дюймах. Поскольку PrintVisual печатает объект типа Visual, страницу можно сформиро- вать, размещая элементы управления и интерфейсные элементы на панели — например, Canvas. Данный способ позволяет работать с графикой из библиотеки Shapes и выводить текст в объектах TextBlock. Программа PrintaBunchaButtons выводит панель Grid с градиентным фоном, на которой размещены 25 кнопок. PrintaBunchaButtons.cs Ц----------------------------------------------------- И PrintaBunchaButtons.cs (с) 2006 by Charles Petzold И----------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media;
400 Глава 17. Печать и диалоговые окна namespace Petzold.Pri ntaBunchaButtons { public class PrintaBunchaButtons : Window { [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new PrintaBunchaButtonsО): } public PrintaBunchaButtonsO Title = "Print a Bunch of Buttons"; SizeToContent = SizeToContent.WldthAndHelght; ResizeMode = ResizeMode.CanMInlmlze; // Создание кнопки 'Print' Button btn = new ButtonO; btn.FontSize = 24; btn.Content = "Print ..."; btn.Padding = new Th1ckness(12); btn.Margin = new Th1ckness(96); btn.Click += PrintOnCl1ck; Content = btn; void Pr1ntOnC11ck(object sender, RoutedEventArgs args) { PrintDialog dig = new PrlntDIalogO; If ((bool)dlg.ShowD1alog().GetValueOrDefault()) { // Создание панели Grid Grid grid = new GrldO; // Определение пяти строк и столбцов // с автоматическим выбором размера for (Int 1 = 0; 1 < 5; 1++) { ColumnDef 1 nition coldef = new ColumnDefinitionO; coldef.Width = GridLength.Auto; gr1d.ColumnDef1nitions.Add(col def); RowDefinition rowdef = new RowDef1n1t1on(); rowdef.Height = GridLength.Auto; grid.RowDef1nit1ons.Add(rowdef); } // Назначение градиентной заливки Grid grid.Background =
Печать и диалоговые окна 401 new L1nearGrad1entBrush(Colors.Gray, Colors.White, new Po1nt(0, 0), new Polntd, D); // Генератор случайных чисел Random rand = new RandomO; // Заполнение Grid 25 кнопками for (Int 1 = 0; 1 < 25; 1++) { Button btn = new ButtonO; btn.FontSize = 12 + rand.Next(8); btn.Content = "Button No. "+(1+1); btn.Horizontal Alignment = HorizontalAlignment.Center; btn.VertlcalAlIgnment = VerticalAlignment.Center; btn.Margin = new Th1ckness(6); grid.Children.Add(btn); Gr1d.SetRow(btn. 1^5); Gr1d.SetColumn(btn, i / 5); // Определение размеров Grid gr1d.Measure(new S1ze(Double.Pos1t1velnfinity, Double.PositIvelnfInity)); Size sIzeGrld = grld.DesIredSIze; // Определение точки для центровки объекта Grid на странице Point ptGrid = new Po1nt((dlg.PrintableAreaWidth - sIzeGrld.Width) / 2, (dlg.PrintableAreaHeight - sIzeGrld.Height) / 2); // Формирование макета gr1d.Arrange(new Rect(ptGr1d, sIzeGrld)); // Печать dig.PrlntVIsual(grid, Title); } } } } Чтобы методика была представлена наиболее наглядно, программа не обла- дает некоторыми возможностями предыдущего примера — например, определе- нием полей страницы или сохранением настроек PrintTicket и PrintQueue. При печати экземпляра класса, производного от UIEIement, необходим еще один важный шаг; для элемента должен быть сформирован макет. Это означает, что для объекта должны быть вызваны методы Measure и Arrange. В противном случае объект будет обладать нулевыми размерами и не будет отображаться на странице. Программа PrintaBunchaButtons использует бесконечные размеры при вызове Measure для объекта Grid; другое решение основано на использовании
402 Глава 17. Печать и диалоговые окна размера страницы. При вызове Arrange программа получает размер Grid из свой- ства DesiredSize, вычисляет точку для размещения Grid в центре страницы, а за- тем вызывает Arrange с передачей вычисленной точки и размеров. Теперь объект Grid можно передавать PrintVisual. Многостраничный документ можно напечатать несколькими вызовами Print- Visual, но в этом случае каждая страница будет считаться отдельным заданием печати. Для более эффективного вывода многостраничных документов програм- ма вызывает метод PrintDocument, определяемый классом PrintDialog. В аргументах PrintDocument передаются экземпляр класса, производного от DocumentPaginator, и текстовая строка с описанием документа для очереди печати. Абстрактный класс DocumentPaginator определяется в пространстве имен System. Windows.Documents. Вы должны определить класс, производный от DocumentPag- inator, а также определить некоторые свойства и методы, помеченные классом DocumentPaginator как абстрактные. Один из таких методов — GetPage — возвра- щает объект типа DocumentPage. Такой объект легко создается на базе объекта Visual, а следовательно, многостраничная печать не так уж сильно отличается от одностраничной. Каждая страница документа представлена объектом Visual. Ваш класс, производный от DocumentPaginator, должен переопределить логиче- ское свойство IsPageCountValid, доступное только для чтения; свойство PageCount, также доступное только для чтения; свойство PageSize, доступное для чтения/за- писи; метод GetPage (получает номер страницы, возвращает объект типа Document- Page); и свойство Source, которое может возвращать null. Вероятно, потомок DocumentPaginator также будет определять некоторые соб- ственные свойства. Допустим, вы пишете программу для печати транспарантов. Когда-то транспаранты печатались на длинных лентах фальцованной бумаги, но в наши дни чаще печатается одна большая буква на страницу. Вероятно, классу, производному от DocumentPaginator, понадобится свойство типа Text, которому программа задает текст выводимой надписи. Скорее всего, потомок Document- Paginator также должен содержать свойство для указания шрифта. Чтобы вам не пришлось определять множество отдельных свойств, большая часть сведений о шрифте обычно объединяется в объекте типа Typeface. Но если вы предпочитаете немного упростить потомка DocumentPaginator, лучше обойтись без дополнительных свойств и просто определить конструктор, которому передается вся необходимая информация. Объект DocumentPaginator обычно существует не так долго, чтобы выбор решения имел сколько-нибудь существенное значение. Скорее всего, объект будет создан по щелчку на кнопке Print в окне PrintDialog и уничтожен после вызова PrintDocument. Далее приводится класс BannerDocumentPaginator, определяющий два свойст- ва: Text и Typeface. BannerDocumentPaginator.cs // // BannerDocumentPaginator.cs (с) 2006 by Charles Petzold // using System:
Печать и диалоговые окна 403 using System.Globalization; using System.Windows; using System.Windows.Controls: using System.Windows.Documents; using System.Windows.Media; namespace Petzold.PrintBanner ( public class BannerDocumentPaginator : DocumentPaginator { string txt = Typeface face = new TypefaceC"); Size sizePage; Size sizeMax = new Size(O, 0); // Открытые свойства, специфические для потомка DocumentPaginator public string Text { set { txt = value; } get { return txt; } } public Typeface Typeface { set { face = value; } get { return face; } } // Приватная функция для создания объекта FormattedText FormattedText GetFormattedText(char ch, Typeface face, double em) { return new FormattedText(ch.ToString(), Cuiturelnfo.CurrentCulture, FlowDirecti on.LeftToRight, face, em. Brushes.Bl ack); } // Необходимые переопределения public override bool IsPageCountValid { get { // Определение максимального размера символов foreach (char ch in txt) { FormattedText formtxt = GetFormattedText(ch. face. 100); sizeMax.Width = Math.Max(sizeMax.Width, formtxt.Width); sizeMax.Height = Math.Max(sizeMax.Height, formtxt.Height); } return true;
404 Глава 17. Печать и диалоговые окна } } public override int PageCount { get { return txt == null ? 0 : txt.Length: } } public override Size PageSize { set { sizePage = value: } get { return sizePage: } } public override DocumentPage GetPagetint numPage) { DrawingVisual vis = new DrawingVisual0: DrawingContext de = vis.RenderOpen(); // При вычислении коэффициента предполагаются поля // размером в полдюйма double factor = Math.Min((PageS1ze.Width - 96) / sizeMax.Width, (PageSize.Height - 96) / sizeMax.Height); FormattedText formtxt = GetFormattedText(txt[numPage], face, factor * 100); // Определение точки для вывода символа по центру страницы Point ptText = new Point((PageSize.Width - formtxt.Width) / 2, (PageSize.Height - formtxt.Height) /2); de.DrawText(formtxt, ptText): dc.CloseO; return new DocumentPage(vis); } public override IDocumentPaginatorSource Source { get { return null; } } } } Как подсказывает само название DocumentPaginator, класс обеспечивает разбие- ние документа на страницы. Он должен определить, сколько страниц содержит документ и что выводится на каждой странице. В данном случае количество страниц определяется просто: оно совпадает с количеством символов в тексто- вой строке. Однако класс также должен определить размер символов. Для этого ему придется сконструировать для каждой буквы объект типа FormattedText, оп- ределяющий высоту и максимальную ширину символов. Если определить конструктор с аргументами, содержащими всю необходимую информацию для разбиения на страницы, всю логику разбиения также можно разместить в самом конструкторе. В решении с определением свойств для разбие- ния придется поискать другое место. На мой взгляд, свойство IsPageCountValid
Печать и диалоговые окна 405 хорошо подойдет для этой цели, потому что оно заведомо вызывается перед вы- полнением каких-либо действий. Итак, в своем свойстве IsPageCountValid класс BannerDocumentPaginator создает объекты FormattedText для каждого символа в стро- ке и сохраняет наибольший из найденных размеров. Метод GetPage отвечает за возврат объектов типа DocumentPage, которые со- здаются из объекта типа Visual. Метод создает объект DrawingVisual, открывает DrawingContext и вычисляет множитель по наибольшему размеру символа и раз- меру страницы за вычетом полей в 0,5 дюйма. Метод конструирует новый объ- ект FormattedText и размещает его по центру страницы вызовом DrawText. Объект DrawingVisual передается конструктору DocumentPage, a GetPage возвращает итого- вый объект. Пользовательский интерфейс программы печати транспарантов довольно тривиален. PrintBanner.cs И // PrintBanner.cs (с) 2006 by Charles Petzold и using System; using System.Print!ng; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media: namespace Petzold.PrintBanner { public class PrintBanner : Window { TextBox txtbox; [STAThread] public static void Main!) { Application app = new ApplicationO; app.Run(new PrintBanner!)); } public PrintBanner!) { Title = "Print Banner"; SizeToContent = SizeToContent.WidthAndHeight; // Создание объекта StackPanel, назначаемого содержимым окна StackPanel stack = new StackPanel!); Content = stack; // Создание объекта TextBox
406 Глава 17. Печать и диалоговые окна txtbox = new TextBoxO; txtbox.Width = 250; txtbox.Margin = new Thickness(12); stack.Chi 1dren.Add(txtbox); // Создание кнопки Button btn = new ButtonO; btn.Content = "_Print..."; btn.Margin = new Thickness(12); btn.Click += PrintOnClick; btn.HorizontalAlignment = HorizontalAlignment.Center; stack.Children.Add(btn): txtbox.Focus(); } void PrintOnClick(object sender, RoutedEventArgs args) { PrintDialog dig = new PrintDialog(); if (dlg.ShowDialogO .GetValueOrDefault()) ( // Принудительное назначение книжной ориентации PrintTicket prntkt = dig.PrintTicket; prntkt.PageOrientation = PageOrientation.Portrait; dig.PrintTicket = prntkt; // Создание нового объекта BannerDocumentPaginator BannerDocumentPaginator paginator = new BannerDocumentPaginator); // Задание свойства Text по содержимому TextBox paginator.Text = txtbox.Text; // Задание свойства PageSize по размерам страницы paginator.PageSize = new Size(dlg.PrintableAreaWidth, dlg.Pri ntableAreaHei ght); // Вывод документа на печать dlg.PrintDocument(paginator, "Banner: " + txtbox.Text); } } } } Программа лучше работает при книжной ориентации страниц, поэтому даже если пользователь выберет альбомную ориентацию, метод PrintOnClick возвраща- ется к книжной. Затем метод создает объект типа BannerDocumentPaginator и за- дает свойства Text и PageSize. Метод завершается передачей инициализирован- ного объекта BannerDocumentPaginator методу PrintDocument, который делает все остальное.
Печать и диалоговые окна 407 Обратите внимание: программа PrintBanner не передает BannerDocumentPagi- nator пользовательский объект Typeface. В действительности программе необхо- димо диалоговое окно выбора шрифта, но, к сожалению, в первой версии WPF такое окно отсутствовало. Мне пришлось самому заняться его написанием. Де- лать это совершенно не хотелось, но выбора не было. Диалоговое окно выбора шрифта обычно строится из элементов ComboBox с постоянно открытыми списками. К сожалению, класс ComboBox в первой вер- сии WPF не поддерживал такого режима, а моя первая попытка имитировать ComboBox из TextBox и ListBox оставляла желать лучшего. Я хотел, чтобы фокус ввода всегда оставался у TextBox, но предотвратить передачу фокуса ListBox ока- залось нелегко. Затем я понял, что начать следует с простого элемента, не полу- чающего фокуса, который бы выглядел и работал как ListBox. Я назвал его Lister. Это первый файл проекта ChooseFont. Lister.cs И---------------------------------------- // Lister.cs (с) 2006 by Charles Petzold и using System: using System.Col lections: using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: namespace Petzold.ChooseFont { class Lister : Contentcontrol { Scroll Vi ewer scroll: StackPanel stack: ArrayList list = new ArrayListO: int indexSelected = -1: // Открытое событие public event EventHandler SelectionChanged: // Конструктор public ListerO Focusable = false: // Объект Border назначается содержимым Contentcontrol Border bord = new BorderO: bord.BorderThickness = new Thickness(l): bord.BorderBrush = SystemColors.ActiveBorderBrush; bord.Background = SystemColors.WindowBrush:
408 Глава 17. Печать и диалоговые окна Content = bond; // Объект Scroll Viewer назначается дочерним объектом Border scroll = new Scroll Vi ewer(); scroll.Focusable = false; scroll.Padding = new Thickness(2, 0, 0, 0); bord.Chi Id = scroll; // Объект StackPanel назначается содержимым Scrollviewer stack = new StackPanel0; scroll.Content = stack; // Установка обработчика для нажатия левой кнопки мыши AddHandler(TextBlock.MouseLeftButtonDownEvent, new MouseButtonEventHandler(TextBlockOnMouseLeftButtonDown)); Loaded += OnLoaded; void OnLoaded(object sender, RoutedEventArgs args) { // Прокрутка элемента Lister при исходном отображении, // чтобы в нем отображался выделенный объект Scroll IntoViewO; } // Открытые методы для добавления элементов в Lister, вставки и т.д. public void Add(object obj) { list.Add(obj): TextBlock txtblk = new TextBlockO: txtblk.Text = obj.ToStringt); stack.Children.Add(txtblk): } public void InsertCint index, object obj) { list.Insert(index, obj): TextBlock txtblk = new TextBlockO: txtblk.Text = obj. ToStrlngO: stack.Chi 1dren.Insert(i ndex. txtblk): } public void Cl ear 0 { Selectedlndex = -1; stack.Chi 1dren.Cl earО; list.ClearO: } public bool Contains(object obj) { return list.Contains(obj):
Печать и диалоговые окна 409 } public int Count { get { return list.Count; } } // Метод, вызываемый для выделения элемента списка // по введенной букве public void GoToLetter(char ch) int offset = Selectedlndex + 1; for (int i = 0: i < Count; 1++) { int index = (i + offset) $ Count; if (Char.Tollpper(ch) == Cha r.ToUpper(1i s t[i ndex].ToSt r i ng()[0])) { Selectedlndex = index; break; } } } // Свойство Selectedlndex отвечает за отображение полосы выделения public int Selectedlndex { set { if (value < -1 || value >= Count) throw new ArgumentOutOfRangeException("SelectedIndex"); if (value == indexSelected) return; if (indexSelected != -1) TextBlock txtblk = stack.Children[indexSelected] as TextBlock; txtblk.Background = SystemColors.WindowBrush; txtblk.Foreground = SystemColors.WindowTextBrush; } indexSelected = value; if (indexSelected > -1) { TextBlock txtblk = stack.Children[indexSelected] as TextBlock; txtblk.Background = SystemColors.HighlightBrush; txtblk.Foreground = SystemColors.HighlightTextBrush; }
410 Глава 17. Печать и диалоговые окна Scrol UntoViewO: // Инициирование события SelectionChanged OnSelectionChanged(EventArgs.Empty); } get { return indexSelected; } } // Свойство Selectedltem использует Selectedlndex public object Selectedltem { set { Selectedlndex = list.IndexOf(value); } get { if (Selectedlndex > -1) return 1istCSelectedlndex]; return null; } } // Открытые методы для постраничной прокрутки списка public void PageUpO { if (Selectedlndex == -1 || Count == 0) return; int index = Selectedlndex - (intMCount * scroll.ViewportHeight / scroll.ExtentHeight); if (index < 0) index = 0; Selectedlndex = index; } public void PageDown0 { if (Selectedlndex == -1 || Count == 0) return; int index = Selectedlndex + (int)(Count * scroll.ViewportHeight / scroll.ExtentHeight); if (index > Count - 1) index = Count - 1; Selectedlndex = index; }
ррматьи диалоговые окна 411 // Приватный метод прокрутки списка, обеспечивающий // отображение текущего выделенного элемента void ScrollIntoViewO { if (Count == 0 || Selectedlndex == -1 || scroll.ViewportHeight > scroll.ExtentHeight) return; double heightPerltem = scroll.ExtentHeight / Count; double offsetltemTop = Selectedlndex * heightPerltem; double offsetltemBot = (Selectedlndex + 1) * heightPerltem; if (offsetltemTop < scroll.Verticaloffset) scroll.ScrollToVerticalOffset(offsetltemTop); else if (offsetltemBot > scroll.Verticaloffset + scroll.ViewportHeight) scroll.ScrollToVerticalOffset(scroll.Verticaloffset + offsetltemBot - scroll.Verticaloffset - scroll.VievjportHeight): } // Обработчик события и триггер void TextBlockOnMouseLeftButtonDown(object sender, MouseButtonEventArgs args) { if (args.Source is TextBlock) Selectedlndex = stack.Children.IndexOf(args.Source as TextBlock); } protected virtual void OnSelectionChanged(EventArgs args) if (SelectionChanged != null) SelectionChanged(this, args); } } } Элемент Lister представляет собой Contentcontrol с рамкой Border, содержащей объект Scrollviewer; последний содержит StackPanel, которая, в свою очередь, содер- жит несколько объектов TextBlock. Метод TextBlockOnMouseLeftButtonDown, находя- щийся в конце файла, задает свойству Selectedlndex индекс объекта TextBlock, на котором был сделан щелчок. Свойство Selectedlndex отвечает за цвета текста и фона объектов TextBlock, обозначающие текущий выделенный объект, а также за инициирование события SelectionChanged вызовом OnSelectionChanged. Клавиатурный интерфейс изменения выделенного объекта обеспечивается за пределами элемента Lister, в следующем классе. Класс TextBoxWithLister тоже яв- ляется производным от Contentcontrol и содержит объект DockPanel с элементами TextBox и Lister. Класс переопределяет метод OnPreviewKeyDown для смены выде- ленного элемента в соответствии с клавишами управления курсором.
412 Глава 17. Печать и диалоговые окна TextBoxWithLister.cs //--------------------------------------------------- // TextBoxWithLister.es (с) 2006 by Charles Petzold //--------------------------------------------------- using System; using System.Col lections; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ChooseFont { class TextBoxWithLister : Contentcontrol { TextBox txtbox; Lister lister; bool isReadOnly; // Открытые события public event EventHandler SelectionChanged; public event TextChangedEventHandler TextChanged; // Конструктор public TextBoxWithListerO { // Создание объекта DockPanel как содержимого элемента DockPanel dock = new DockPanelO; Content = dock; // Объект TextBox пристыковывается наверху txtbox = new TextBox(); txtbox.TextChanged += TextBoxOnTextChanged; dock.Chi 1dren.Add(txtbox); DockPanel.SetDock(txtbox, Dock.Top); // Объект Lister заполняет оставшуюся площадь DockPanel lister = new ListerO; lister.SelectionChanged += ListerOnSelectionChanged; dock.Chi 1dren.Add(lister); } // Открытые свойства, относящиеся к объекту TextBox public string Text { get { return txtbox.Text; } set { txtbox.Text = value; }
Печать и диалоговые окна 413 --------- public bool IsReadOnly { set { IsReadOnly = value; } get { return IsReadOnly; } } // Другие открытые свойства для взаимодействия с элементом Lister public object Selectedltem { set { lister.Selectedltem = value; If (lister.Selectedltem != null) txtbox.Text = lister. Selectedltem. ToStrlngO; else txtbox.Text = } get return lister.Selectedltem; } } public Int Selectedlndex { set { lister.Selectedlndex = value; If (lister.Selectedlndex == -1) txtbox.Text = else txtbox.Text = lister.Selectedltem.ToStrlngO; } get { return lister.Selectedlndex; } } public void Add(object obj) { lister.Add(obj); } public void Insertdnt index, object obj) { lister.Insert(index, obj); } public void ClearO { lister.ClearO;
414 Глава 17. Печать и диалоговые окна } public bool Contains(object obj) { return lister.Contains(obj): } // Передача фокуса по щелчку кнопкой мыши protected override void OnMouseDown(MouseButtonEventArgs args) { base.OnMouseDown(args); Focus 0; ) // Полученный фокус ввода передается объекту TextBox protected override void OnGotKeyboardFocus( KeyboardFocusChangedEventArgs args) { base.OnGotKeyboardFocus(args); if (args.NewFocus == this) { txtbox. FocusO; if (Selectedlndex == -1 && lister.Count > 0) Selectedlndex = 0: } } // Нажатия буквенных клавиш передаются методу GoToLetter // объекта Lister protected override void OnPreviewTextInput( TextCompositionEventArgs args) { base.OnPreviewTextlnput(args): If (IsReadOnly) { 1ister.GoToLetter(args.Text[0]); args.Handled = true; } } // Обработка клавиш управления курсором // для смены выделенного объекта protected override void OnPreviewKeyDown(KeyEventArgs args) { base.OnKeyDown(args): if (Selectedlndex == -1) return; switch (args.Key) {
Печать и диалоговые окна 415 case Key.Home: If (lister.Count > 0) Selectedlndex = 0; break: case Key.End: if (lister.Count > 0) Selectedlndex = lister.Count - 1: break: case Key.Up: if (Selectedlndex > 0) Selectedlndex--: break: case Key.Down: if (Selectedlndex < lister.Count - 1) SelectedIndex++: break; case Key.PageUp: lister.PageUpO: break: case Key.PageDown: lister.PageDown(): break: default: return: } args.Handled = true; } // Обработчики событий и триггеры void ListerOnSelectionChanged(object sender, EventArgs args) { if (Selectedlndex == -1) txtbox.Text = else txtbox.Text = lister.Selectedltem.ToStrlngO: OnSelectionChanged(args): } void TextBoxOnTextChanged(object sender, TextChangedEventArgs args) { if (TextChanged != null) TextChanged(this, args); } protected virtual void OnSelectionChanged(EventArgs args) { if (SelectionChanged != null) SelectionChanged(this, args): }
416 Глава 17. Печать и диалоговые окна Класс работает в двух режимах в зависимости от значения свойства IsRead- Only. Если свойство равно false, пользователь может ввести любое значение в Text- Box, а программа, использующая элемент, получает введенные данные. Этот режим используется окном FontDialog для размера шрифта. Списковая часть элемента предоставляет набор стандартных размеров, но пользователь может ввести лю- бое другое значение. Если свойство IsReadOnly истинно, в объекте TextBox всегда отображается вы- деленный элемент и ручной ввод данных невозможен. Любые нажатия бук- венных клавиш передаются методу GoToLetter объекта Lister, который пытается выбрать следующую строку, начинающуюся с указанной буквы. Такой режим хорошо подходит для выбора семейства шрифтов, размера, насыщенности и дру- гих параметров шрифтов, отображаемых в окне FontDialog. Чтобы окно остава- лось как можно более простым, я решил запретить пользователю выбор или ввод любых значений, информация о которых отсутствует в системе. Независимо от значения свойства IsReadOnly, фокус ввода всегда остается у объ- екта TextBox. Элемент получает оповещения через обработчик события о щелчках на объектах в списке, но списковая секция не получает фокус от таких дейст- вий. Переопределение OnPreviewKeyDown обеспечивает весь основной клавиатур- ный интерфейс элемента за счет обработки клавиш управления курсором. Класс FontDialog содержит пять элементов TextBoxWithLister (для отображения семейств шрифтов, начертаний, насыщенности, сжатия и размеров), пять надпи- сей, идентифицирующих элементы, еще один элемент Label для отображения об- разца текста, а также кнопки ОК и Cancel. Тем не менее настоящие сложности создают не эти элементы, а динамическое содержимое элементов. Первый элемент TextBoxWithLister выводит список всех доступных семейств шрифтов. Для их получения используется статический метод Fonts.SystemFont- Families, а элемент заполняется информацией ближе к концу конструктора. Од- нако разные семейства обладают разными наборами начертаний, насыщенности и сжатия. Каждый раз, когда пользователь выбирает новое семейство шрифтов, обработчик события FamilyOnSelectionChanged класса FontDialog должен очистить три элемента TextBoxWithLister и заполнить их заново по содержимому коллек- ции FamilyTypefaces выделенного объекта FontFamily. Класс FontDialog определяет всего два открытых свойства: свойство Typeface инкапсулирует свойства FontFamily, Fontstyle, FontWeight и FontStretch, а свойство FaceSize (тип double) содержит размер шрифта. (Я не мог присвоить ему имя FontSize, потому что сам класс FontDialog наследует свойство FontSize, которое управляет размером шрифта, используемого в элементах диалогового окна!) FontDialog.cs //------------------------------------------ // FontDialog.cs (с) 2006 by Charles Petzold //------------------------------------------ using System; using System.Col 1ections.Generic: using System.Windows:
Прцятьидиалоговые окна 417 using System.Windows.Controls: using System.Windows.Input: uSing System.Windows.Media: namespace Petzold.ChooseFont public class FontDialog : Window TextBoxWithLister boxFamily, boxStyle, boxWeight, boxStretch. boxSize: Label IblDisplay: bool islIpdateSuppressed = true; // Открытые свойства public Typeface Typeface set { if (boxFamily.Contains(value.FontFamily)) boxFamily.Selectedltem = value.FontFamily: else boxFamily.Selectedlndex = 0: if (boxStyle.Contains(value.Style)) boxStyle.Selectedltem = value.Style: else boxStyle.Selectedlndex = 0; i f (boxWei ght.Conta i ns(va1ue.Wei ght)) boxWeight.Selectedltem = value.Weight; else boxWeight.Selectedlndex = 0: i f (boxStretch.Contains(va1ue.Stretch)) boxStretch.Selectedltem = value.Stretch: else boxStretch.Selectedlndex = 0: ) get { return new Typeface((FontFamily)boxFamily.Selectedltem, (FontStyle)boxSty1e.Seiectedltem, (FontWeight)boxWeight.Seiectedltem, (FontStretch)boxStretch.Seiectedltem): } } public double FaceSize { set
418 Глава 17. Печать и диалоговые окна { double size = 0.75 * value; boxSize.Text = size. ToStrlngO; if (!boxSize.Contains(size)) boxSize.Insert(0. size); boxSize.Selectedltem = size; } get { double size; if ({Double.TryParse(boxSize.Text, out size)) size = 8.25; return size / 0.75; } } // Конструктор public FontDialogO { Title = "Font"; ShowInTaskbar = false; Windowstyle = Windowstyle.Tool Window; WindowStartupLocation = WindowStartupLocation.CenterOwner; SizeToContent = SizeToContent.WidthAndHeight; ResizeMode = ResizeMode.NoResize; // Создание панели Grid с тремя строками как содержимого окна Grid gridMain = new GridO; Content = gridMain; // Строка для элементов TextBoxWithLister RowDefinition rowdef = new RowDefinitionO; rowdef.Height = new GridLength(200, GridUnitType.Pixel): gri dMa i n.RowDefi ni ti ons.Add(rowdef); // Строка для образца текста rowdef = new RowDefinitionO; rowdef.Height = new GridLength(150, GridUnitType.Pixel); gri dMa i n.RowDefi ni ti ons.Add(rowdef); // Строка для кнопок rowdef = new RowDefinitionO; rowdef.Height = GridLength.Auto; gri dMa1n.RowDefi ni ti ons.Add(rowdef); // Единственный столбец панели Grid ColumnDefinition coldef = new ColumnDefinitionО;
Пеиятьидиалоговые окна 419 coldef.Width = new GridLength(650, GridUnitType.Pixel); gri dMai n.ColumnDef1ni 11ons.Add(col def); // Создание панели Grid с двумя строками и пятью столбцами // для элементов TextBoxWithLister Grid gridBoxes = new GridO; gri dMai n.Chi 1dren.Add(gri dBoxes): // Строка для элементов Label rowdef = new RowDefinition(); rowdef.Height = GridLength.Auto; gridBoxes.RowDefi ni ti ons.Add(rowdef): // Строка для элементов EditBoxWithLister rowdef = new RowDefinition(); rowdef.Height = new GridLengthdOO. GridUnitType.Star); gri dBoxes.RowDefi ni ti ons.Add(rowdef); // Первый столбец (FontFamily) coldef = new ColumnDefinitionO; coldef.Width = new GridLength(175, GridUnitType.Star); gri dBoxes.ColumnDefi ni ti ons.Add(col def); // Второй столбец (Fontstyle) coldef = new ColumnDefinitionO; coldef.Width = new GridLengthdOO, GridUnitType.Star); gri dBoxes.ColumnDefi ni ti ons.Add(col def); // Третий столбец (FontWeight) coldef = new ColumnDefinitionO; coldef.Width = new GridLengthdOO, GridUnitType.Star); gridBoxes.ColumnDefi ni ti ons.Add(col def); // Четвертый столбец (FontStretch) coldef = new ColumnDefinitionO; coldef.Width = new GridLengthdOO. GridUnitType.Star); gri dBoxes.ColumnDefi ni ti ons.Add(col def); // Пятый столбец (FaceSize) coldef = new ColumnDefinitionO; coldef.Width = new GridLength(75, GridUnitType.Star); gri dBoxes.ColumnDefi ni ti ons.Add(col def); // Создание надписи и элементов TextBoxWithLister // для FontFamily Label Ibl = new Label О; Ibl.Content = "Font Family";
420 Глава 17. Печать и диалоговые окна Ibl.Margin = new Thlckness(12, 12. 12. 0); gridBoxes.Children.Adddbl): Grid.SetRowdbl, 0); Grid.SetColumndbl. 0): boxFamily = new TextBoxWithLister(): boxFamily.IsReadOnly = true: boxFamily.Margin = new Thickness(12, 0. 12. 12): gri dBoxes.Children.Add(boxFami1у); Grid.SetRow(boxFamily. 1): Grid.SetColumn(boxFamily. 0); // Создание надписи и элементов TextBoxWithLister // для Fontstyle Ibl = new Label(): Ibl.Content = "Style"; Ibl.Margin = new lhickness(12, 12, 12, 0): gridBoxes.Children.Adddbl): Grid.SetRowdbl, 0): Grid.SetColumndbl, 1); boxStyle = new TextBoxWithLister(); boxStyle.IsReadOnly = true: boxStyle.Margin = new lhickness(12, 0, 12. 12): gri dBoxes.Children.Add(boxSty1e); Grid.SetRow(boxStyle, 1): Grid.SetColumn(boxStyle, 1): // Создание надписи и элементов TextBoxWithLister // для FontWeight Ibl = new Label(); Ibl.Content = "Weight"; Ibl.Margin = new Thickness(12, 12, 12, 0); gridBoxes.Children.Adddbl); Grid.SetRowdbl, 0); Grid.SetColumndbl, 2): boxWeight = new TextBoxWithLister(); boxWeight.IsReadOnly = true: boxWeight.Margin = new Thickness(12, 0, 12, 12); gri dBoxes.Chi 1dren.Add(boxWei ght): Grid.SetRow(boxWeight, 1): Grid.SetColumn(boxWeight, 2): // Создание надписи и элементов TextBoxWithLister // для FontStretch Ibl = new Label(); Ibl.Content = "Stretch":
Печать и диалоговые окна 421 Ibl.Margin = new Thickness(12, 12, 12, 0): gridBoxes.Children.Add(Ibl): Grid.SetRowdbl, 0): Grid.SetColumndbl, 3): boxStretch = new TextBoxWithLister(); boxStretch.IsReadOnly = true; boxStretch.Margin = new Thickness(12. 0, 12, 12): gri dBoxes.Chi 1dren.Add(boxStretch); Grid.SetRow(boxStretch, 1); Grid.SetColumn(boxStretch, 3): // Создание надписи и элементов TextBoxWithtister // для FaceSize Ibl = new Label(): Ibl.Content = "Size"; Ibl.Margin = new Thickness(12, 12, 12, 0); gridBoxes.Children.Add(Ibl): Grid.SetRowdbl. 0): Grid.SetColumndbl, 4); boxSize = new TextBoxWithListerO: boxSize.Margin = new Thickness(12, 0, 12. 12); gridBoxes.Children.Add(boxSi ze); Grid.SetRow(boxSize, 1); Grid.SetColumn(boxSize, 4): // Создание элемента Label для вывода образца текста IblDispl ay = new Label О: IblDisplay.Content = "AaBbCc XxYzZz 012345": 1 bl Di splay.Hori zonta1ContentAli gnment = Horizonta1 Alignment.Center; 1 bl Di splay.Verti ca1ContentAli gnment = Verti cal Ali gnment.Center; gri dMa i n.Chi 1dren.Add(IblDisplay): Grid.SetRowdblDisplay. 1); // Создание панели Grid с пятью столбцами для кнопок Grid gridButtons = new GridO: gridMain.Chi 1dren.Add(gri dButtons): Grid.SetRow(gridButtons. 2); for (int i = 0: i < 5: i++) gridButtons.ColumnDefinitions.Add(new Co IumnDefinition()); // Кнопка OK Button btn - new ButtonO: btn.Content = "OK"; btn.IsDefault = true: btn.HorizontalAlignment = HorizontalAlignment.Center;
422 Глава 17. Печать и диалоговые окна btn.MinWidth = 60: btn.Margin = new Thickness(12); btn.Click += OkOnClick; gri dButtons.Children.Add(btn): Grid.SetColumn(btn. 1); // Кнопка Cancel btn = new ButtonO: btn.Content = "Cancel": btn.IsCancel = true: btn.Horizontal Alignment = HorizontalAlignment.Center: btn.MinWidth = 60: btn.Margin = new Thickness(12): gri dButtons.Children.Add(btn); Grid.SetColumn(btn, 3): // Инициализация FontFamily системным списком семейств шрифтов foreach (FontFamily fam in Fonts.SystemFontFamilies) boxFamily.Add(fam): // Инициализация поля FontSize doublet] ptsizes = new doublet] { 8, 9. 10, 11, 12. 14, 16. 18. 20. 22. 24. 26, 28, 36. 48. 72 }: foreach (double ptsize in ptsizes) boxSize.Add(ptsize); // Установка обработчиков событий boxFamily.SelectionChanged += FamilyOnSelectionChanged: boxStyle.SelectionChanged += StyleOnSelectionChanged: boxWeight.SelectionChanged += StyleOnSelectionChanged: boxStretch.SelectionChanged += StyleOnSelectionChanged: boxSize.TextChanged += SizeOnTextChanged: // Инициализация начального выделения по свойствам Window // (Вероятно, значения будут изменены при задании свойств) Typeface = new Typeface(FontFamily. Fontstyle, FontWeight. Fontstretch); FaceSize = FontSize: // Передача фокуса ввода boxFamily.FocusO; // Разрешить обновление образца текста isUpdateSuppressed = false; UpdateSampleO; } // Обработчик события SelectionChanged для boxFamily void FamilyOnSelectionChanged(object sender. EventArgs args)
Печать и диалоговые окна 423 { // Получение выделенного семейства шрифтов FontFamily fntfam = (FontFamily)boxFamily.Selectedltem; // Сохранение предыдущих значений Fontstyle, // FontWeight, FontStretch. // Значения равны null только при первом вызове метода Fontstyle? fntstyPrevious = (FontStyle?)boxStyle.Selectedltem: FontWeight? fntwtPrevious = (FontWei ght?)boxWei ght.Seiectedltem: FontStretch? fntstrPrevious = (FontStretch?)boxStretch.Seiectedltem; // Отключение вывода образца текста islIpdateSuppressed = true; // Очистка полей Fontstyle, FontWeight и FontStretch boxStyle.ClearO; boxWeight.ClearO; boxStretch.Cl ear(); // Перебор гарнитур в выделенном семействе foreach (FamilyTypeface ftf in fntfam.FamilyTypefaces) { // Размещение начертаний в boxStyle // (список всегда начинается с Normal) if (!boxStyle.Contains(ftf.Style)) { if (ftf.Style == FontStyles.Norma1) boxStyle.Insert(0, ftf.Style); else boxStyle.Add(ftf.Style); } // Размещение насыщенности в boxWeight // (список всегда начинается с Normal) if (!boxWeight.Contains(ftf.Weight)) { if (ftf.Weight == FontWeights.Normal) boxWeight.Insert(0, ftf.Weight); else boxWei ght.Add(ftf.Wei ght): // Размещение сжатия в boxStretch // (список всегда начинается с Normal) if (!boxStretch.Contains(ftf.Stretch)) { if (ftf.Stretch == FontStretches.Normal) boxStretch.Inserted, ftf.Stretch);
424 Глава 17. Печать и диалоговые окна else boxStretch.Add(ft f.St retch); } } // Назначение выделенной строки в boxStyle 1f (boxSty1e.Conta1 ns(fntstyPrevIous)) boxStyle.Selectedltem = fntstyPrevious; else boxStyle.Selectedlndex = 0; // Задание выделенной строки в boxWeight 1f (boxWeight.Contains(fntwtPrev1ous)) boxWeight.Selectedltem = fntwtPrevious; else boxWeight.Selectedlndex = 0; // Назначение выделенной строки в boxStretch i f (boxStretch.Contai ns(fntstrPrevious)) boxStretch.Selectedltem = fntstrPrevious; else boxStretch.Selectedlndex = 0; // Разрешаем и проводим обновление образца isUpdateSuppressed = false; UpdateSampleO; } // Обработчик события SelectionChanged для boxStyle. // boxWeight и boxStretch void StyleOnSelectionChanged(object sender, EventArgs args) { UpdateSampleO: } // Обработчик события TextChanged для поля размера void SizeOnTextChanged(object sender, TextChangedEventArgs args) { UpdateSample(); // Обновление образца текста void UpdateSampleO { if OsUpdateSuppressed) return; IblDisplay.FontFamily = (FontFamily)boxFamily.Selectedltem; IblDisplay.Fontstyle = (FontStyle)boxStyle.Selectedltem; IblDisplay.FontWeight = (FontWeight)boxWeight.Selectedltem;
Печать и диалоговые окна 425 IblDisplay.FontStretch = (FontStretch)boxStretch.SeiectedItem; double size; if (!Double.TryParse(boxSize.Text, out size)) size = 8.25; IblDisplay.FontSize = size / 0.75; } // Кнопка OK уничтожает диалоговое окно void OkOnClick(object sender, RoutedEventArgs args) DialogResult = true; } } } Для проверки FontDialog программа ChooseFont просто создает кнопку в сере- дине клиентской области. Каждый раз, когда пользователь щелкает на кнопке, окно появляется на экране. Если пользователь закрывает диалоговое окно кноп- кой ОК, программа задает шрифтовые свойства основного окна по значениям, выбранным в диалоговом окне. Естественно, эти свойства наследуются кнопкой. ChooseFont.cs //------------------------------------------- И ChooseFont.cs (с) 2006 by Charles Petzold И-------------------------------------------- using System: using System.Windows; using System.Windows.Controls: using System.Windows.Input; using System.Windows.Media: namespace Petzold.ChooseFont public class ChooseFont : Window { [STAThread] public static void MainO { Application app = new Application); app.Run(new ChooseFontO): } public ChooseFontO { Title = "Choose Font"; Button btn = new ButtonO; btn.Content = Title: btn.HorizontalAlignment = HorizontalAlignment.Center: btn.Vertical Alignment = VerticalAlignment.Center;
426_________________________________ Глава 17. Печать и диалоговые окна ----- btn.Click += ButtonOnClick; Content = btn; void ButtonOnClick(object sender. RoutedEventArgs args) { FontDialog dig = new FontDialogO; dig.Owner = this; // Задание свойств FontDialog по свойствам Window dig.Typeface = new Typeface(FontFamily. Fontstyle. FontWeight. FontStretch); dig.FaceSize = FontSize; if (dig.ShowDia1og().GetVa1ueOrDefault()) { // Задание свойств Window по свойствам FontDialog FontFamily = dig.Typeface.FontFamily; Fontstyle = dig.Typeface.Style; FontWeight = dig.Typeface.Weight; FontStretch = dig.Typeface.Stretch; FontSize = dig.FaceSize; } } } } Без долгих разговоров, я приведу улучшенную версию программы печати транспарантов. Для этого проекта потребуется ссылка на файл BannerDocument- Paginator.cs, а также на три файла, используемых классом FontDialog: Lister.cs, TextBoxWithLister.cs и FontDialog.cs. Новая версия программы содержит кнопку Font для вызова окна FontDialog. Выбранная гарнитура просто передается Banner- DocumentPaginator. Естественно, выбранный размер игнорируется, потому что BannerDocumentPaginator вычисляет его на основании размера страницы. PrintBetterBanner.cs //--------------------------------------------------- // PrintBetterBanner.cs (с) 2006 by Charles Petzold //--------------------------------------------------- using Petzold.ChooseFont; using Petzold.PrintBanner; using System; using System.Printing; using System.Windows: using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.Pri ntBetterBanner
раиять и диалоговые окна 427 public class PrintBetterBanner : Window ( TextBox txtbox: Typeface face; [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new Pri ntBetterBanner()); public PrintBetterBanner() { Title = "Print Better Banner"; SizeToContent = SizeToContent.WidthAndHeight; // Создание объекта StackPanel как содержимого окна StackPanel stack = new StackPanelО; Content = stack; // Создание объекта TextBox txtbox = new TextBoxO; txtbox.Width = 250; txtbox.Margin = new Thickness(12); stack.Chi 1dren.Add(txtbox); // Создание кнопки Font Button btn = new ButtonO; btn.Content = "_Font..."; btn.Margin = new Thickness(12); btn.Click += FontOnClick; btn.HorizontalAlignment = HorizontalAlignment.Center; stack.Children.Add(btn); // Создание кнопки Print btn = new ButtonO; btn.Content = "_Print..."; btn.Margin = new Thickness(12); btn.Click += PrintOnClick; btn.HorizontalAlignment = HorizontalAlignment.Center; stack.Chi 1dren.Add(btn); // Инициализация поля Facename face = new Typeface(FontFamily, Fontstyle, FontWeight, FontStretch); txtbox.Focus О; void FontOnClick(object sender, RoutedEventArgs args) {
428 Глава 17. Печать и диалоговые окна FontDialog dig = new FontDIalog(); dig.Owner = this: dig.Typeface = face; if (dig.ShowDia1og().GetVa1ueOrDefault()) { face = dig.Typeface; } } void PrintOnClick(object sender, RoutedEventArgs args) { PrintDialog dig = new PrintDialogO: if (dig.ShowDialog().GetVa1ueOrDefault()) { // Принудительное назначение книжной ориентации PrintTicket prntkt = dig.PrintTicket; prntkt.PageOrientation = PageOrientation.Portrait; dig.PrintTicket = prntkt; // Создание нового объекта DocumentPaginator BannerDocumentPaginator paginator = new BannerDocumentPaginatorO; // Задание свойства Text по содержимому TextBox paginator.Text = txtbox.Text; // Задание свойства Typeface по содержимому поля paginator.Typeface = face; // Задание свойства PageSize по размерам страницы paginator.PageSize = new Size(dlg.PrintableAreaWidth, dlg.Pri ntableAreaHei ght); // Вызов PrintDocument для печати документа dig.PrintDocument(paginator. "Banner: " + txtbox.Text); } } Класс BannerDocumentPaginator всего лишь демонстрирует основы многостра- ничной печати; в отношении практической пользы он значительно уступает программам, распределяющим текст по страницам. При использовании несколь- ких гарнитур такой объект получается достаточно сложным, но даже при работе с простым текстом, оформленным единым шрифтом, потомок DocumentPaginator получается нетривиальным. Несмотря на все сложности, для клона Блокнота, представленного в следую- щей главе, потребуется класс разбиения, поддерживающий работу с простым текстом и перенос по строкам. Класс PlainTextDocumentPaginator (также представ- ленный в следующей главе) является немаловажной частью этого проекта.
1 Клон Блокнота Ъ процессе изучения новой операционной системы или программного интерфей- са обычно наступает момент, когда программист смотрит на типичное приложе- ние и говорит: «Я тоже смогу написать такую программу». Чтобы доказать это, программист даже может попытаться создать клон приложения. Вероятно, слово «клон» здесь не совсем уместно, потому что новая программа не имеет ничего общего с генетической копией. Программист стремится воспроизвести пользо- вательский интерфейс и функциональность без дублирования кода, защищенно- го авторским правом! Приложение, представленное в этой главе, весьма близко к стандартному при- ложению Блокнот (Notepad) системы Microsoft Windows — как по внешнему виду, так и по поведению и функциональности. Единственной важной функци- ей, которую я не стал воспроизводить, является окно справки (в главе 25 будет показано, как реализовать справочную систему приложения). Клон Блокнота поддерживает работу с файлами, печать, поиск и замену, диалоговое окно выбо- ра шрифта, а также сохраняет пользовательские настройки между сеансами. В проект необходимо включить ссылки на файл PrintMarginDialog.cs из преды- дущей главы, а также на три файла, используемые FontDialog: FontDialog.cs, Lister.cs и TextBoxWithLister.cs. Все остальные файлы, составляющие Клон Блокнота, при- водятся в этой главе. Конечно, текстовых редакторов в мире и так хватает, но написание Клона Блокнота не является сугубо учебным упражнением. В главе 20 после добавле- ния еще нескольких файлов он превратится в новую программу, которую я на- звал XML Cruncher. Эта программа станет мощным инструментом для изуче- ния и экспериментов с языком XAML во второй части книги. Поскольку два класса программы XML Cruncher являются производными от классов Клона Блокнота, в Клоне иногда встречается нетривиальный код, ориентированный на наследование. Такие случаи будут оговорены особо. Первый файл проекта представляет собой простую последовательность дирек- тив C# attribute, обеспечивающих создание метаданных в файле NotepadClone.exe. Метаданные идентифицируют программу, содержат информацию об авторских правах и версии. Подобный файл следует включать в любое «полноценное» приложение.
430 Глава 18. Клон Блокнота NotepadCloneAssemblyInfo.cs //--------------------------------------------------------- // NotepadCloneAssemblyInfo.cs (с) 2006 by Charles Petzold //--------------------------------------------------------- using System.Reflection: [assembly: AssemblyTitleCNotepad Clone")] [assembly: AssemblyProduct("NotepadClone")] [assembly: AssemblyDescriptionC’Functionally Similar to Windows Notepad")] [assembly: Assemblycompany("www.charlespetzold.com")] [assembly: AssemblyCopyright("\x00A9 2006 by Charles Petzold")] [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFi1eVersion("1.0.0.0")] Файл не требует особого обращения по сравнению со всеми остальными файлами проекта, содержащими исходный код С#; он компилируется вместе со всеми остальными файлами. Это единственный файл Клона Блокнота, не вхо- дящий в проект XAML Cruncher, — последний содержит собственный информа- ционный файл. Я уже упоминал о том, что Клон Блокнота сохраняет информацию о пользо- вательских настройках между сеансами. В наши дни стандартным форматом для хранения конфигурационных данных является XML. Как правило, прило- жения хранят настройки пользовательского уровня в изолированной области, называемой «пользовательскими данными приложения». Например, для про- граммы Notepadclone, распространяемой компанией Petzold и установленной пользователем Deirdre, конфигурационные данные будут храниться в каталоге \Documents and Settings\Deirdre\Application Data\Petzold\NotepadClone. Однако программу XAML Cruncher также можно загрузить с моего веб-сайта с использованием механизма установки ClickOnce; для установок такого типа рекомендуется хранить конфигурационные данные в области, называемой «локаль- ными пользовательскими данными приложения»: \Documents and Settings\Deirdre\ Local Settings\Application Data\Petzold\NotepadClone. Хотя обе области принадлежат конкретному пользователю, обычные пользовательские данные приложения предназначены для перемещаемых пользователей (то есть пользователей, входя- щих в систему на другом компьютере), тогда как локальные пользовательские данные относятся к конкретному пользователю на конкретном компьютере. Пользовательскую конфигурацию удобно определить в виде открытых полей или свойств класса, предназначенных специально для этой цели. После этого можно будет использовать класс XmlSerializer для автоматического сохранения и загрузки информации в формате XML. Процесс сериализации (сохранения или загрузки файла) начинается с создания объекта XmlSerializer для класса, который требуется сериализовать: XmlSerializer xml = new Xml Seri alizer(typeof(MyClass)): Далее объект типа MyClass сохраняется в формате XML вызовом метода Serialize. Для определения файла потребуется объект типа Stream, XmlWriter или TextWriter
Клон Блокнота 431 (производным от которого является StreamWriter). Метод Deserialize класса Xml- Serializer преобразует XML в объект типа, заданного в конструкторе XmlSerializer. Метод Deserialize возвращает объект типа object, который преобразуется к нуж- ному типу. Как нетрудно предположить, класс XmlSerializer использует рефлексию для анализа класса, указанного в конструкторе XmlSerializer, и его членов. Согласно документации, XmlSerializer генерирует код в пространстве исполнения програм- мы. По этой причине любой класс, предназначенный для сериализации, опреде- ляется как открытый. XmlSerializer сохраняет и загружает только поля и свойства, определяемые как открытые, и игнорирует все члены, доступные только для чтения или записи. При десериализации XmlSerializer создает объект подходяще- го типа с использованием непараметризованного конструктора класса, после чего задает свойства объекта на основании XML. XmlSerializer не работает с клас- сами, не имеющими непараметризованного конструктора. Если какие-либо из сериализуемых членов класса относятся к составному типу данных, такие классы и структуры тоже должны быть открытыми и иметь непараметризованные конструкторы. XmlSerializer рассматривает открытые поля и свойства этих других классов и структур, доступные для чтения/записи, как вложенные элементы. Кроме того, XmlSerializer может работать со свойствами, ко- торые представляют собой массивы и объекты List. Далее приводится класс NotepadCloneSettings. Для удобства и экономии места я определил открытые члены класса в виде полей, а не в виде свойств. NotepadCloneSettings.cs //----------------------------------------------------- И NotepadCloneSettings.cs (с) 2006 by Charles Petzold И------------------------------------------------------ using System: using System.10; using System.Windows: using System.Windows.Media: using System.Xml.Serialization: namespace Petzold.Notepadclone public class NotepadCloneSettings . { // Конфигурация по умолчанию public Windowstate Windowstate = WindowState.Normal: public Rect RestoreBounds = Rect.Empty: public Textwrapping Textwrapping = Textwrapping.NoWrap: public string FontFamily = : public string Fontstyle = new FontStyleConverter0.ConvertToString(FontStyles.Normal): public string Fontweight = new FontWeightConverter(),ConvertToString(FontWeights.Normal);
432 Глава 18. Клон Блокнота public string FontStretch = new FontStretchConverter().ConvertToStr1ng(FontStretches.Normal); public double FontSize = 11; // Сохранение настроек в файле public virtual bool Save(str1ng strAppData) { try { Dlrectory.CreateDIrectory( Path.GetDIrectoryName(strAppData)); StreamWrlter write = new StreamWrlter(strAppData); Xml Serializer xml = new XmlSerlallzer(GetTypeO); xml.Ser1al1ze(wr1te, this); write.Close(); } catch { return false: } return true; // Загрузка настроек из файла public static object Load(Type type, string strApoData) { StreamReader reader; object settings; Xml Serializer xml = new XmlSerlalIzer(type): try reader = new StreamReader(strAppData): settings = xml.Deseriallze(reader); reader.Closet): } catch { settings = type.GetConstructort System.Type.EmptyTypes).Invoketnul1): } return settings; } } } Класс содержит два метода. Метод Save получает полностью уточненное имя файла и создает объекты StreamWriter и XmlSerializer для заполнения полей. Метод Load объявлен статическим, потому что он отвечает за создание объекта
Клон Блокнота 433 типа NotepadCloneSettings — либо вызовом Deserialize, либо (если конфигурацион- ный файл еще не существует) с использованием конструктора класса. Метод получился Load более сложным, чем необходимо, потому что файл бу- дет повторно использован при наследовании в проекте XAML Cruncher. Если бы класс не предназначался для повторного наследования, то параметр Туре метода Load был бы не нужен, а аргумент конструктора XmlSerializer принял бы вид typeof(NotepadCloneSettings). Вместо использования рефлексии для вызо- ва конструктора класса в блоке catch хватило бы простого вызова new Notepad- CloneSettings(). Как видно из листинга, в первых трех полях содержатся два перечисления и структура. Их сериализация и десериализация проходят нормально. Тем не менее с большинством шрифтовых свойств дело обстоит сложнее. Например, Fontstyle представляет собой структуру, не содержащую открытых полей или свойств. Мне пришлось явно определить шрифтовые поля Notepad- CloneSettings как объекты string, а затем инициализировать их при помощи раз- личных классов (например, FontStyleConverter), часто используемых при разборе кода XAML. Следующим идет файл Notepadclone.cs. Если вам кажется, что этот файл слишком мал, чтобы быть главным файлом проекта, вы абсолютно правы. Как обычно, мой класс Notepadclone объявлен производным от Window, но я разделил исходный код этого класса на семь частей при помощи ключевого слова partial. Разные файлы примерно соответствуют командам меню верхнего уровня. На- пример, код файла Notepadclone.Edit.cs тоже является частью класса Notepadclone, но он посвящен созданию и обработке событий меню Edit. Помните, что эти семь файлог образуют один класс и обладают полным дос- тупом к методам и полям, определенным в других файлах. В частности, элемент TextBox, занимающий основную часть клиентской области программы, хранится в поле с именем txtbox. Доступ к этому объекту необходим многим методам. Файл Notepadclone.cs содержит конструктор Notepadclone. В начале своей ра- боты конструктор обращается к некоторым метаданным сборки, определенным в файле NotepadCloneAssemblyInfo.cs, для получения названия программы (строка «Notepad Clone») и пути к файлу с конфигурационными данными. Это сделано с расчетом на будущее использование класса при наследовании. Затем конструк- тор размещает в окне элементы DockPanel, Menu, StatusBar и TextBox. Конструиро- вание меню практически полностью вынесено в другие файлы. NotepadClone.cs //--------------------------------------------- fl NotepadClone.cs (с) 2006 by Charles Petzold И---------------------------------------------- using System; using System.ComponentModel; using System.10; using System.Reflection;
434 Глава 18. Клон Блокнота using System.Windows: using System.Windows.Controls: using System.Windows.Controls.Primitives: using System.Windows.Input: using System.Windows.Media: namespace Petzold.Notepadclone { public partial class Notepadclone : Window { protected string strAppTitle: // Имя программы для заголовка protected string strAppData; // Полный путь к файлу настроек protected NotepadCloneSettings settings: // Настройки protected bool isFileDirty = false: // Флаг изменения содержимого // Элементы, используемые в главном окне protected Menu menu: protected TextBox txtbox: protected StatusBar status: string strLoadedFile: // Полное имя загруженного файла StatusBarltem statLineCol: // Строка и столбец [STAThread] public static void MainO { Application app = new ApplicationO: app.ShutdownMode = ShutdownMode.OnMainWindowClose: app.Run(new NotepadCloneO): public NotepadCloneO { // Получение исполняемой сборки для обращения к атрибутам Assembly asmbly = Assembly.GetExecutingAssemblyO: // Получение атрибута AssemblyTitle для поля strAppTitle AssemblyTitleAttribute title = (AssemblyTitleAttribute)asmbly. GetCustomAttributes(typeof(AssemblyTitleAttribute), false)[0]: strAppTitle = title.Title: // Получение атрибута AssemblyProduct для поля strAppData AssemblyProductAttribute product = (AssemblyProductAttribute)asmbly. GetCustomAttributes(typeof(AssemblyProductAttribute), false)[0]: strAppData = Path.Combine( Envi ronment.GetFolderPath( Envi ronment.Special Folder.LocalApplicationData),
Клон Блокнота 435 "PetzoldW" + product. Product + "W" + product.Product + ".Settings.xml"); // Создание объекта DockPanel как содержимого окна DockPanel dock = new DockPanel(); Content = dock; // Создание меню, пристыкованного у верхнего края menu = new MenuO; dock.Children.Add(menu); DockPanel.SetDock(menu. Dock.Top); // Создание строки состояния, пристыкованной у нижнего края status = new StatusBarO; dock.Children.Add(status): DockPanel.SetDock(status, Dock.Bottom); // Создание объекта StatusBarltem // для отображения строки и столбца statLineCol = new StatusBarItem(); statLineCol.HorizontalAlignment = HorizontalAlignment.Right; status.Items.Add(statLineCol); DockPanel.SetDock(statLineCol. Dock.Right); // Создание объекта TextBox, заполняющего оставшуюся площадь // клиентской области txtbox = new TextBoxO; txtbox.AcceptsReturn = true; txtbox.AcceptsTab = true; txtbox.VerticalScrol1BarVisi bi 1ity = Scrol1BarVisi bi 1ity.Auto; txtbox.Horizontal ScrollBarVisibility = ScrollBarVi si bi 1i ty.Auto; txtbox.TextChanged += TextBoxOnTextChanged; txtbox.SelectionChanged += TextBoxOnSelectionChanged; dock.Children.Add(txtbox); // Создание всех команд меню верхнего уровня AddFileMenu(menu); // файл Notepadclone.File.cs AddEditMenu(menu); // файл Notepadclone.Edit.cs AddFormatMenu(menu); // файл NotepadClone.Format.es AddViewMenu(menu); // файл NotepadClone.View.es AddHelpMenu(menu); // файл NotepadClone.Help.es // Загрузка настроек, сохраненных при предыдущем запуске settings = (NotepadCloneSettings) LoadSettings(); // Применение сохраненных настроек Windowstate = settings.Windowstate; if (settings.RestoreBounds != Rect.Empty) {
436 Глава 18. Клон Блокнота Left = settings.RestoreBounds.Left: Top = set tings.RestoreBounds.Top: Width = settings.RestoreBounds.Width: Height = settings.RestoreBounds.Height: txtbox.Textwrapping = settings.TextWrappi ng: txtbox.FontFamily = new FontFamily(settings.FontFamily); txtbox.Fontstyle = (Fontstyle) new FontStyleConverter(). ConvertFromString(settings.FontStyle); txtbox.Fontweight = (FontWeight) new FontWeightConverter() ConvertFromString(settings.FontWeight); txtbox.FontStretch = (FontStretch) new FontStretchConverter(). ConvertFromString(settings.FontStretch): txtbox.FontSize = settings.FontSize: // Установка обработчика для события Loaded Loaded += WindowOnLoaded; // Передача фокуса TextBox txtbox.Focus(); } // Переопределяемый метод для загрузки настроек, // вызывается из конструктора protected virtual object LoadSettings() { return NotepadCloneSettings.Load(typeof(NotepadCloneSettings), strAppData): } // Обработчик события Loaded: имитирует команду New // с возможной загрузкой файла из командной строки void WindowOnLoaded(object sender, RoutedEventArgs args) { ApplicationCommands.New.Execute(nul1, this): // Получение аргументов командной строки string^] strArgs = Environment.GetCommandLineArgs(): if (strArgs.Length > 1) // Первый аргумент - имя программы! if (Fi1e.Exists(strArgs[1J)) LoadFi1e(strArgsQI]); } else { MessageBoxResult result = MessageBox.Show("Cannot find the " +
Клон Блокнота 437 Path.GetFI1eName(strArgsC1]) + ” file.\r\n\r\n" + "Do you want to create a new file?", strAppTitie, MessageBoxButton.YesNoCancel, MessageBoxImage.Question); // Кнопка "Cancel" закрывает окно if (result == MessageBoxResult.Cancel) Close(); // Кнопка "Yes" создает и закрывает файл else if (result == MessageBoxResult.Yes) { try File.CreatetstrLoadedFile = strArgs[l]).Close(); catch (Exception exc) { MessageBox.Show("Error on File Creation: " + exc.Message, strAppTitle, MessageBoxButton.OK, MessageBoxImage.Asteri sk); return: } UpdateTitle(): } // Кнопка "No" не выполняет действий } } } // Событие OnClosing: проверяем, можно лишь уничтожить файл protected override void OnClosing(CancelEventArgs args) { base.OnClosing(args); args.Cancel = !OkToTrash(): settings.RestoreBounds = RestoreBounds; // Событие OnClosed: задаем поля 'settings' и вызываем SaveSettings protected override void OnClosed(EventArgs args) { base.OnClosed(args): settings.Windowstate = Windowstate; settings.Textwrapping = txtbox.Textwrapping: settings.FontFamily = txtbox.FontFamily.ToStringO: settings.Fontstyle = new FontSty1 eConverter().ConvertToString(txtbox.Fontstyle);
438 Глава 18. Клон Блокнота settings.FontWeight = new FontWeightConverter(). ConvertToString(txtbox.FontWeight): settings.FontStretch = new FontStretchConverter(). ConvertToString(txtbox.FontStretch); settings.FontSize = txtbox.FontSize: SaveSettingsO; } // Переопределяемый метод для вызова метода Save // для объекта 'settings' protected virtual void SaveSettingsO { settings.Save(strAppData); } // Метод UpdateTitle выводит имя файла или "Untitled" protected void UpdateTitle() { if (strLoadedFile == null) Title = "Untitled - " + strAppTitle: else Title = Path.GetFileName(strLoadedFile) + " - " + strAppTitle: } // При изменении текста TextBox просто установить флаг isFileDirty void TextBoxOnTextChanged(object sender, RoutedEventArgs args) isFileDirty = true; } // При изменении выделения обновляется строка состояния void TextBoxOnSelectionChanged(object sender, RoutedEventArgs args) { int iChar = txtbox.Selectionstart: int iLine = txtbox.GetLinelndexFromCharacterlndex(iChar): // Проверка ошибки if (iLine == -1) { statLineCol.Content = ""; return: } int iCol = iChar - txtbox.GetCharacterlndexFromLinelndex(iLine): string str = String.FormatC'Line {0} Col {1}". iLine + 1, iCol + 1): if (txtbox.SelectionLength > 0) {
Клон Блокнота 439 IChar += txtbox.SeiectionLength; ILine = txtbox.GetLinelndexFromCharacterlndex(iChar); ICol = IChar - txtbox.GetCharacterlndexFromLinelndex(iLine); str += String.Format(" - Line {0} Col {1}", iLine + 1, iCol + 1); } statLineCol.Content = str; } } } В конце конструктора класс Notepadclone вызывает виртуальный метод Load- Settings для загрузки конфигурационного файла, а также устанавливает обработ- чик события Loaded. Обработчик Loaded проверяет аргументы командной строки (и, возможно, загружает указанный файл). Все методы, относящиеся к операци- ям с файлами, находятся в файле NotepadClone.File.cs (см. далее). Файл NotepadClone.cs завершается обработчиками событий TextChanged и Selec- tionChanged объектов TextBox. Обработчик TextChanged просто устанавливает флаг isFileDirty, чтобы при выходе программа предложила пользователю сохранить вне- сенные изменения. Обработчик SelectionChanged отвечает за вывод номеров теку- щей строки и столбца (или выделенного фрагмента) в строке состояния. Метод AddFileMenu из файла NotepadClone.File.cs отвечает за построение меню File и выполнение всех команд, связанных с файловыми операциями. Многие команды меню File соответствуют статическим свойствам класса ApplicationCom- mands; для их выполнения может использоваться механизм привязки команд. NotepadClone.File.cs и И NotepadClone.File.cs (с) 2006 by Charles Petzold // using Microsoft.Win32; using System; using System.10; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Printing; namespace Petzold.Notepadclone { public partial class Notepadclone ; Window { // Фильтр для диалоговых окон File>Open и Save protected string strFilter = "Text Documents(*.txt)I*.txt|All Files(*.*)|*.*"; void AddFileMenu(Menu menu)
440 Глава 18. Клон Блокнота { // Создание команды меню верхнего уровня File Menu Item ItemFile = new MenuItemO: ItemFile.Header - "_F11e"; menu.Items.Add(ltemFlle); // Команда меню New Menuitem ItemNew = new MenuItemO: ItemNew.Header = "_New": ItemNew.Command = Appl1cationCommands.New: 1temF11e.Iterns.Add(itemNew); CommandB1nd1ngs.Add( new CommandBIndlng(Applicat1onCommands.Nev/, NewOnExecute)); // Команда меню Open Menuitem ItemOpen = new MenuItemO: ItemOpen.Header = "_Open..."; ItemOpen.Command = Appl1cationCommands.Open: 1temF11e.Iterns.Add(1temOpen); CommandBIndlngs.Add( new CommandBIndlng(Appl1cationCommands.Open, OpenOnExecute)): // Команда меню Save Menuitem itemSave = new MenuItemO: ItemSave.Header = "_Save": ItemSave.Command = Appl1cationCommands.Save; ItemFile.Items.Add(ItemSave); CommandBIndlngs.Add( new CommandBindlng(Appl1cationCommands.Save, SaveOnExecute)); // Команда меню Save As Menuitem ItemSaveAs = new MenuItemO: ItemSaveAs.Header = "Save _As..."; ItemSaveAs.Command = ApplicationCommands.SaveAs; ItemFile.Items.Add(ItemSaveAs); CommandBindlngs.Add( new CommandBindlng(Appl1cationCommands.SaveAs. SaveAsOnExecute)); // Разделители и команды печати ItemFile. Items,Add(new SeparatorO) ; AddPrlntMenuItemsdtemFI le): ItemFile. Items .Add(new Separator)): // Команда меню Exit Menuitem itemExIt = new MenuItemO:
Клон Блокнота 441 ItemExIt.Header = ,,E_xit": ItemExit.Click += ExitOnClick; itemFile.Items.Add(itemExit): } // Команда меню File>New: начало работы с пустым объектом TextBox protected virtual void NewOnExecute(object sender, ExecutedRoutedEventArgs args) { if (IQkToTrashO) return; txtbox.Text = ,,H: strLoadedFile = null: isFileDirty = false: UpdateTitle(): } // Команда меню File>0pen: вызов диалогового окна и загрузка файла void OpenOnExecute(object sender, ExecutedRoutedEventArgs args) { if (!OkToTrashO) return; OpenFileDialog dig = new OpenFi1 eDialog(); dig.Filter = strFilter; if ((bool)dlg.ShowDialog(this)) { LoadFi1e(dlg.Fi1 eName): } // Команда f-ile>Save: возможное выполнение SaveAsExecute void SaveOnExecute(object sender, ExecutedRoutedEventArgs args) if (strLoadedFile == null || strLoadedFile.Length == 0) DisplaySaveDialogC"): else SaveFile(strLoadedFile); // Команда File>Save As; вызов диалогового окна и сохранение файла void SaveAsOnExecute(object sender, ExecutedRoutedEventArgs args) Di splaySaveDi alog(strLoadedFi1e); // Метод вызывает диалоговое окно Save и возвращает true. // если файл был сохранен bool DisplaySaveDialog(string strFileName)
442 Глава 18. Клон Блокнота { SaveFileDialog dig = new SaveFileDialogO: dig.Filter = strFilter; dlg.FileName = strFileName: If ((bool )dlg.ShowDialog(this)) { SaveFile(dlg.FileName): return true; } return false; // для OkToTrash // Команда File>Exit: просто закрыть окно void ExitOnClick(object sender, RoutedEventArgs args) { Close(); } // Метод OkToTrash возвращает true, если содержимое TextBox // не нуждается в сохранении bool OkToTrashO { if (!1sFI1eDIrty) return true; MessageBoxResult result = MessageBox.ShowCThe text in the file " + strLoadedFile + " has changed\n\n" +"Do you want to save the changes?", strAppTitle, MessageBoxButton.YesNoCancel, MessageBoxImage.Questi on, MessageBoxResult.Yes); if (result == MessageBoxResult.Cancel) return false; else if (result == MessageBoxResult.No) return true; else // result == MessageBoxResult.Yes { if (strLoadedFile != null && strLoadedFile.Length > 0) return SaveFile(strLoadedFile): return DisplaySaveDialogC"): } // Метод LoadFile выводит окно сообщения при обнаружении ошибки void LoadFi1е(string strFileName) { try {
Клон Блокнота 443 txtbox.Text = Flle.ReadAllText(strFlleName); } catch (Exception exc) { MessageBox.Show("Error on File Open: " + exc.Message, strAppT1tie, MessageBoxButton.OK, MessageBoxImage.Aster1sk); return; } strLoadedFIle = strFi1 eName; UpdateTItleO; txtbox.SeiectlonStart = 0: txtbox.SeiectlonLength = 0: IsFileDirty = false; } // Метод SaveFIle выводит окно сообщения при обнаружении ошибки bool SaveFI1е(strlng strFIleName) { try { File.Wr1teAl1 Text(strF11 eName, txtbox.Text); } catch (Exception exc) { MessageBox.Show("Error on File Save" + exc.Message, strAppT1t1e, MessageBoxButton.OK, MessageBoxImage.Asterisk); return false; strLoadedFIle = strFIleName; UpdateTItleO; IsFileDirty = false; return true; } } } Метод OkToTrash спрашивает у пользователя, действительно ли он хочет по- терять несохраненные изменения. Две команды печати меню File вынесены в следующий файл с исходным кодом, NotepadClone.Print.cs. Логика файла весьма сходна с кодом печати из предыдущей главы. Принципиальное различие состоит в том, что используемый в этом фай- ле класс PlainTextDocumentPaginator обладает несколькими свойствами (Typeface, Facesize и т. д.), сходными со свойствами FontDialog; в данном случае программный код получает их значения от TextBox. Другое свойство PlainTextDocumentPaginator, TextWrapping, управляет переносом текста по строкам.
444 Глава 18. Клон Блокнпт^ NotepadClone.Print.cs //---------------------------------------------------- // Notepadclone.Print.cs (с) 2006 by Charles Petzold //---------------------------------------------------- using Petzold.PrlntWIthMarglns; // для PageMarginsDialog using System; using System.Windows: using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Printing; namespace Petzold.Notepadclone { public partial class Notepadclone : Window { // Поля для печати PrintQueue prnqueue: PrintTicket prntkt; Thickness marginPage = new Th1ckness(96): void AddPr1ntMenuItems(MenuItem ItemFile) { // Команда меню Page Setup Menuitem ItemSetup = new MenuItemO: ItemSetup.Header = "Page Set_up...": ItemSetup. Click += PageSetupOnCUck; 1temF11e.Iterns.Add(1temSetup): // Команда меню Print Menuitem ItemPrlnt = new MenuItemO: ItemPrlnt.Header = "-Print...": ItemPrlnt.Command = Appl1cationCommands.Print: ItemFile.Items.Add(ItemPrlnt); CommandBindings.Add( new CommandBinding(Appl1cationCommands.Print, PrlntOnExecuted)): } void PageSetupOnClick(object sender. RoutedEventArgs args) { // Создание диалогового окна // и инициализация свойства PageMargins PageMarginsDialog dig = new PageMarglnsDIalogO: dig.Owner = this; dig.PageMargins = marginPage; If (dlg.ShowD1alog().GetValueOrDefault()) { // Сохранение полей страницы
^^нБлокнота _______________________________________________________________________ marginPage = dig.PageMarglns; } } void PrintOnExecuted(object sender. ExecutedRoutedEventArgs args) PrintDialog dig = new PrintDialog(); // Получение значений PrintQueue и PrintTicket // от предыдущих вызовов if (prnqueue ! = null) dig.PrintQueue = prnqueue; if (prntkt != null) prntkt = dig.PrintTicket; if (dlg.ShowDia 1og().GetValueOrDefault()) // Сохранение значений PrintQueue и PrintTicket /7 из диалогового окна prnqueue = dig.PrintQueue. prntkt = dig.PrintTicket; // Создание объекта PlainTextDocumentPaginator Pl a inTextDocument Paginator paginator = new PlainTextDocumentPaginator(); // Задание свойств объекта paginator paginator.PrintTicket = prntkt; paginator.Text = txtbox.Text; paginator.Header = strLoadedFile; paginator.Typeface = new Typeface(txtbox.FontFamily. txtbox.Fontstyle, txtbox.FontWeight. txtbox.FontStretch); paginator.FaceSize = txtbox.FontSize; paginator.Textwrapping = txtbox.Textwrapping: paginator.Margins = marginPage: paginator.PageSize = new Size(dlg.PrintableAreaWidth, dig.PrintableAreaHeight); // Печать документа dlg.PrintDocument(paginator, Title); } } } Разумеется, большая часть «черной работы» выполняется классом PlainText- DocumentPaginator. Я определил в классе текстовое свойство Header, которому Клон Блокнота задает имя загруженного файла. Кроме того, я хотел, чтобы объ- ект печатал в конце страницы завершитель, состоящий из номера текущей стра- ницы и общего числа страниц (например, «Page 5 of 15»).
446 Глава 18. Клон Блокнота Основное форматирование выполняется методом Format класса PlainTextDoc- umentPaginator. Метод перебирает все текстовые строки в файле (помните, что в режиме переноса каждая строка текста фактически представляет собой от- дельный абзац). Каждая строка передается методу ProcessLine, который с учетом переноса разбивает строку текста на одну или несколько печатных строк. Каж- дая печатная строка сохраняется в объекте Printline. После того как все объекты Printline будут успешно созданы, количество страниц легко вычисляется по раз- мерам страницы, размерам полей и общему количеству строк. Метод Format за- вершается выводом текста вызовами DrawText. PlainTextDocumentPaginator.cs //------------------------------------------------------------ // PlainTextDocumentPaginator.cs (с) 2006 by Charles Petzold //------------------------------------------------------------ using System; using System.Col 1ectlons.Gener 1 c; using System.Globallzatlon; using System.10: using System.Printing; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media; namespace Petzold.Notepadclone { public class PlainTextDocumentPaginator : DocumentPaginator { // Приватные поля, в том числе ассоциированные // с открытыми свойствами chart] charsBreak = new chart] { ' }; string txt = : string txtHeader = null; Typeface face = new TypefaceC"); double em = 11; Size slzePage = new S1ze(8.5 * 96, 11 * 96); Size sIzeMax = new S1ze(0, 0); Thickness margins = new Th1ckness(96); PrlntTIcket prntkt = new PrintTicketO; Textwrapping txtwrap = Textwrapping.Wrap; // Каждая страница сохраняется в виде объекта DocumentPage L1st<DocumentPage> UstPages: // Открытые свойства public string Text
Клон Блокнота 447 set { txt = value; } get { return txt: } public Textwrapping Textwrapping { set { txtwrap = value: } get { return txtwrap: } } public Thickness Margins { set { margins = value: } get { return margins: } } public Typeface Typeface { set { face = value: } get { return face: } } public double FaceSize { set { em = value: } get { return em: } } public PrintTicket PrintTicket { set { prntkt = value: } get { return prntkt: } } public string Header { set { txtHeader = value: } get { return txtHeader: } // Необходимые переопределения public override bool IsPageCountValld { get { If (UstPages == null) Format(): return true: } } public override Int PageCount { get {
448 Глава 18. Клон Блокнота If (11stPages == null) return 0; return listPages.Count; } } public override Size PageSize { set { sizePage = value; } get { return sizePage; } } public override DocumentPage GetPage(int numPage) { return listPages[numPage]; / public override IDocumentPaginatorSource Source { get { return null; } } // Внутренний класс, указывающий, нужно ли печатать стрелку // в конце строки class Printline { public string String; public bool Flag: public PrintLine(string str, bool flag) { String = str; Flag = flag; } } // Форматирование всего документа на страницы void Format() { // Сохранение каждой строки документа // в виде объекта LineWithFlag List<PrintLine> 1istLines = new List<PrintLine>(); // Используется в некоторых вычислениях FormattedText formtxtSample = GetFormattedTextCW"); // Ширина печатной строки double width = PageSize.Width - Margins.Left - Margins.Right; // Серьезная проблема: спасайся, кто может if (width < formtxtSample.Width) return;
l^jipH Блокнота 449 string strLine; Pen pn = new Pen(Brushes.Black, 2); StrlngReader reader = new StrlngReader(txt); // Вызов ProcessLIne для сохранения каждой строки в listLInes while (null != (strLine = reader.ReadLlneO)) ProcessL1ne(strL1ne, width, listLInes); reader.CloseO; // Подготовка к печати страниц double helghtLIne = formtxtSample.LlneHelght + formtxtSample.Helght; double height = PageSize.Helght - Margins.Top - Margins.Bottom; Int UnesPerPage = (1nt)(he1ght / helghtLIne); // Серьезная проблема: спасайся, кто может If (UnesPerPage < 1) return; Int numPages = (UstLInes.Count + UnesPerPage - 1) / UnesPerPage; double xStart = Margins.Left; double yStart = Margins.Top; // Создание объекта List для хранения объектов DocumentPage UstPages = new L1st<DocumentPage>(); for (Int IPage =0, ILIne = 0; IPage < numPages; 1Page++) { // Создание объекта DrawingVisual // и открытие графического контекста DrawingVisual vis = new DrawingVisualО; DrawingContext de = vls.RenderOpenO; // Вывод заголовка в начале страницы If (Header != null && Header.Length > 0) { FormattedText formtxt = GetFormattedText(Header); formtxt.SetFontWelght(FontWelghts.Bold); Point ptText = new Po1nt(xStart, yStart - 2 * formtxt.Helght); dc.DrawText(formtxt, ptText); } // Вывод завершителя в конце страницы If (numPages > 1) { FormattedText formtxt = GetFormattedText("Page " + (1 Page+1) + " of " + numPages);
450 Глава 18. Клон Блокнота formtxt.SetFontWei ght(FontWei ghts.Bol d); Point ptText = new Point( (PageSize.Width + Margins.Left - Margins.Right - formtxt.Width) / 2, PageSize.Height - Margins.Bottom + formtxt.Height); de.DrawText(formtxt. ptText): // Перебор строк на странице for (int i = 0; i < 1inesPerPage; i++. iLine++) { if (iLine == listLines.Count) break: // Подготовка информации для вывода строки string str = 1istLinesLiLine].String; FormattedText formtxt = GetFormattedText(str); Point ptText = new Point(xStart, yStart + i * heightLine): de.DrawText(formtxt. ptText): // Возможный вывод стрелки if (1istLinesCiLine].Flag) { double x = xStart + width + 6: double у = yStart + i * heightLine + formtxt.Baseline: double len = face.CapsHeight * em: dc.DrawLine(pn, new Point(x, y). new Point(x + len. у - len)); dc.DrawLine(pn, new Point(x, y). new Point(x. у - len / 2)); dc.DrawLine(pn. new Point(x, y). new Point(x + len / 2, y)); } } dc.CloseO; // Создание объекта DocumentPage на основе DrawingVisual DocumentPage page = new DocumentPage(vis); listPages.Add(page); } reader.Close(); } // Преобразование строки текста в несколько печатных строк void ProcessLine(string str, double width. List<PrintLine> list) { str = str.TrimEnd(' ');
Клон Блокнота 451 // Textwrapping == Textwrapping.NoWrap. // ------------------------------------- If (Textwrapping == TextWrapping.NoWrap) { do { Int length = str.Length; while (GetFormattedText(str.Substr1ng(0, length)). Width > width) length--; 11st.Add(new Pr1ntL1ne(str.Substring^, length). length < str.Length)); str = str.Substrlng(length); while (str.Length > 0); } // Textwrapping == TextWrapping.Wrap или // TextWrapping.WrapWIthOverf1ow. // ------------------------------------------------ else { do { Int length = str.Length; bool flag = false; while (GetFormattedText(str.Substr1ng(0, length)). Width > width) { Int Index = str.LastIndexOfAny(charsBreak. length - 2); If (Index != -1) length = Index + 1; // Включая завершающий // пробел или дефис else { // В этой точке мы знаем, что следующий // возможный перенос по пробелу или дефису // выходит за границы допустимой ширины. // Проверить наличие переноса. Index = str.IndexOfAny(charsBreak); If (Index != -1) length = Index + 1; // Если TextWrapping.WrapWIthOverflow, просто // вывести строку. Если TextWrapping.Wrap, // перенести с пометкой. If (Textwrapping == TextWrapping.Wrap) { while (GetFormattedText(str.Substring(
452 Глава 18. Клон Блокнота 0. length)).Width > width) 1ength--: flag = true; } break: // Выход из цикла while } } list.Add(new PrintLine(str.Substring^, length), flag)); str = str.Substring(length): } while (str.Length > 0); } } // Приватный метод для создания объекта FormattedText FormattedText GetFormattedText(string str) { return new FormattedText(str. Cultureinfo.Currentculture. FlowDirection.LeftToRight, face. em. Brushes.Bl ack); } } } Конечно, самой сложной частью класса является метод ProcessLine, который разбивает каждую строку текста на печатные строки. Теоретически метод дол- жен разбивать строки на фрагменты с учетом пробелов и дефисов, а также теку- щего режима переноса. В предыдущих версиях текстовых полей (таких, как TextBox) перенос был либо включен, либо выключен. Иначе говоря, текстовое поле либо переносило длинные строки в границах элемента, либо отсекало слишком длинные строки по правому краю. В WPF-версии TextBox режим переноса представлен свойством TextWrapping, принимающим значения из одноименного перечисления (NoWrap, Wrap и Wrap- WithOverflow). Новая схема пытается решить небольшую проблему: что произой- дет, если перенос текста включен, а слишком длинное слово не помещается по ширине TextBox? В режиме TextWrapping.Wrap сначала выводится как можно большая часть слова, а остальные символы отображаются в следующей строке. В режиме TextWrapping.WrapWithOverflow длинные слова не разбиваются. Конечно, различия между Wrap и WrapWithOverflow создали некоторые проблемы с програм- мированием, но меня больше интересовало, как сделать что-нибудь разумное в режиме NoWrap. Поскольку класс PlainTextDocumentPaginator также использует- ся в проекте XAML Cruncher, я хотел, чтобы печатный вывод степени напоми- нал страницы Visual Studio. Если строка не помещается в заданных границах, редактор Visual Studio позволяет ей «перетечь» на следующую строку и рисует на полях маленькую стрелку — признак продолжения строки. Именно по этой причине PlainTextDocumentPaginator хранит каждую печатную строку в объекте типа Printline.
клон Блокнота 453 Класс Printline содержит логическое поле Flag, которое указывает, нужно ли выводить стрелку в конце строки. Стрелки выводятся в длинных строках в ре- жиме NoWrap, а также и в режиме Wrap, когда длинное слово приходится разби- вать между двумя строками. Интересно заметить, что приложение Блокнот системы Windows использует одинаковую логику переноса при печати документов независимо от действую- щего режима переноса. Перейдем к меню Edit, состоящему как из простых, так и относительно слож- ных команд. К первой категории относятся команды работы с буфером обмена; вы уже знаете, как они реализуются. NotepadClone.Edit.cs //------------------------------------------------- И Notepadclone.Edit.cs (с) 2006 by Charles Petzold П-------------------------------------------------- using System: using System.Windows; using System.Windows.Controls: using System.Windows.Input: namespace Petzold.Notepadclone { public partial class Notepadclone void AddEditMenu(Menu menu) { // Меню Edit верхнего уровня Menuitem itemEdit = new MenuItemO: itemEdit.Header = "_Edit": menu.Items.Add(itemEdit): // Команда меню Undo Menuitem itemUndo = new MenuItemO: itemUndo.Header = "_Undo": itemUndo.Command = ApplicationCommands.Undo: itemEdit.Items.Add(itemUndo): CommandBi ndi ngs.Add(new CommandBi ndi ng( ApplicationCommands.Undo, UndoOnExecute, UndoCanExecute)); // Команда меню Redo Menuitem itemRedo = new MenuItemO; itemRedo.Header = "_Redo": itemRedo.Command = ApplicationCommands.Redo: itemEdit.Items.Add(itemRedo); CommandBindings.Add(new CommandBinding( ApplicationCommands.Redo. RedoOnExecute, RedoCanExecute)); itemEdit.Items.Add(new Separator()):
454 Глава 18. Клон Блокнота // Команды меню Cut. Copy. Paste и Delete Menuitem ItemCut = new MenuItemO; itemCut.Header = "Cu_t"; itemCut.Command = ApplicationCommands.Cut; itemEdit.Iterns.Add(itemCut); CommandBi ndi ngs.Add(new CommandBi ndi ng( ApplicationCommands.Cut. CutOnExecute. CutCanExecute)); Menuitem itemCopy = new MenuItemO; itemCopy.Header = "_Copy"; itemCopy.Command = ApplicationCommands.Copy; itemEdit.Iterns.Add(itemCopy); CommandBi ndi ngs.Add(new CommandBi ndi ng( ApplicationCommands.Copy. CopyOnExecute. CutCanExecute)); Menuitem itemPaste = new MenuItemO; itemPaste.Header = "_Paste"; itemPaste.Command = ApplicationCommands.Paste; itemEdit.Items.Add(itemPaste); CommandBi ndi ngs.Add(new CommandBi ndi ng( ApplicationCommands.Paste, PasteOnExecute. PasteCanExecute)); Menuitem itemDel = new MenuItemO; itemDel.Header = "De_lete"; itemDel.Command = ApplicationCommands.Delete; itemEdit.Items.Add(itemDel); CommandBi ndi ngs.Add(new CommandBi ndi ng( Applicati onCommands.Delete. DeleteOnExecute. CutCanExecute)); itemEdit. Items. Add (new SeparatorO): // Команды Find. FindNext и Replace добавляются // отдельным методом AddFindMenuItems(itemEdit); i temEdi t.Iterns.Add(new Separator()); // Команда меню Select All Menuitem itemAll = new MenuItemO; itemAll.Header = "Select _A11"; itemAll.Command = ApplicationCommands.SeiectAl1; itemEdit.Iterns.Add(itemAll); CommandBi ndi ngs.Add(new CommandBi ndi ng( ApplicationCommands.SeiectAl1. SeiectAllOnExecute)): // Для команды Time/Date потребуется // специальный объект RoutedUICommand InputGestureCollection coll = new InputGestureCol lectionO; coll.Add(new KeyGesture(Key,F5)); RoutedUICommand commTimeDate =
Клон Блокнота 455 new RoutedUICommand("Time/_Date", "TimeDate", GetTypeO, coll); Menuitem itemDate = new MenuItemO; itemDate.Command = commTimeDate; itemEdit.Items.Add(itemDate); CommandBi ndi ngs.Add( new CommandBinding(commTimeDate, TimeDateOnExecute)); } // Обработчики событий Redo void RedoCanExecute(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = txtbox.CanRedo; } void RedoOnExecute(object sender, ExecutedRoutedEventArgs args) { txtbox.Redo(); } // Обработчики событий Undo void UndoCanExecute(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = txtbox.CanUndo; } void UndoOnExecute(object sender. ExecutedRoutedEventArgs args) { txtbox. UndoO; } // Обработчики событий Cut void CutCanExecute(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = txtbox.SeiectedText.Length > 0; void CutOnExecute(object sender, ExecutedRoutedEventArgs args) { txtbox.Cut0; } // Обработчики событий Copy и Delete void CopyOnExecute(object sender, ExecutedRoutedEventArgs args) { txtbox. CopyO; } void DeleteOnExecute(object sender, ExecutedRoutedEventArgs args) { txtbox.SeiectedText = }
456 Глава 18. Клон Блокнота // Обработчики событий Paste void PasteCanExecute(object sender. CanExecuteRoutedEventArgs args) { args.CanExecute = Clipboard.ContainsTextO; } void PasteOnExecute(object sender, ExecutedRoutedEventArgs args) { txtbox. PasteO; } // Обработчик SelectAl1 void SelectAl10nExecute(object sender, ExecutedRoutedEventArgs args) { txtbox.SelectAlK); } // Обработчик Time/Date void TimeDateOnExecute(object sender, ExecutedRoutedEventArgs args) { txtbox. Sei ectedText = DateTime.Now.ToStringO; } } } Вторую, относительно сложную категорию меню Edit составляют команды Find, Find Next и Replace, в работе которых задействованы немодальные диалого- вые окна. Окна Find и Replace настолько похожи, что я написал абстрактный класс FindReplaceDialog, содержащий все элементы, общие для диалоговых окон поиска/замены. Класс получился умеренно сложным, а его код в основном свя- зан с размещением элементов на панели Grid на поверхности диалогового окна. Так как диалоговое окно является немодальным, оно несколько отличается от других классов, производных от Window. В нем присутствует кнопка Cancel, но в немодальных окнах эта кнопка не обязательна, так как пользователь может закрыть диалоговое окно обычным способом. Соответственно, все функции кноп- ки Cancel сводятся к простому вызову Close. Для немодальных диалоговых окон почти всегда необходимы открытые со- бытия. FindReplaceDialog определяет три события FindNext, Replace и ReplaceAII для остальных кнопок окна. При помощи этих событий немодальное диалоговое окно оповещает окно-владельца о том, что пользователь нажал одну из кнопок и ожи- дает реакции. FindReplaceDialog.cs //--------------------------------------------------- // FindReplaceDialog.cs (с) 2006 by Charles Petzold using System; using System.Windows; using System.Windows.Controls;
Клон Блокнота 457 namespace Petzold.Notepadclone ( abstract class FindReplaceDialog : Window // Открытые события public event EventHandler FindNext; public event EventHandler Replace; public event EventHandler ReplaceAll; // Защищенные поля protected Label IblRepl ace; protected TextBox txtboxFind, txtboxReplace; protected CheckBox checkMatch; protected GroupBox groupDirecti on; protected RadioButton radioDown, radiollp: • protected Button btnFind, btnReplace, btnAl1; // Открытые свойства public string FindWhat { set { txtboxFind.Text = value; } get { return txtboxFind.Text; } } public string ReplaceWith { set { txtboxReplace.Text = value; } get { return txtboxReplace.Text; } } public bool MatchCase { set { checkMatch.IsChecked = value; } get { return (bool)checkMatch.IsChecked; } } public Direction Direction { set { if (value == Direction.Down) radioDown.IsChecked = true; else radiollp. IsChecked = true; } get { return (bool)radioDown.IsChecked ? Di recti on.Down Di recti on.Up; } }
458 Глава 18. Клон Блокнота // Защищенный конструктор (так как класс является абстрактным) protected FlndReplaceDialog(WIndow owner) { // Задание основных свойств диалогового окна ShowInTaskbar = false; WindowStyle = WindowStyle.Tool Window; SizeToContent = SizeToContent.WidthAndHeight; WindowStartupLocation = WindowStartupLocation.CenterOwner; Owner = owner; // Создание объекта Grid из трех строк и столбцов, // с автоматическим выбором размера Grid grid = new Grid(); Content = grid; for (int i = 0; i < 3; i++) { RowDefinition rowdef = new RowDefinitionO; rowdef.Height = GridLength.Auto; grid.RowDefi ni ti ons.Add(rowdef); ColumnDefinition coldef = new ColumnDefinition(); coldef.Width = GridLength.Auto; grid.ColumnDefi ni ti ons.Add(col def); } // Искомый текст; объекты Label и TextBox Label Ibl = new Label(); Ibl.Content = "Fi_nd what;"; Ibl.VerticalAlignment = VerticalAlignment.Center; Ibl.Margin = new Thickness(12); grid.Children.Adddbl); Grid.SetRowdbl, 0); Grid.SetColumndbl, 0); txtboxFind = new TextBoxO; txtboxFind.Margin = new Thickness(12); txtboxFind.TextChanged += FindTextBoxOnTextChanged; gri d.Ch i1dren.Add(txtboxF i nd); Grid.SetRow(txtboxFind, 0); Grid.SetColumn(txtboxFind, 1); // Текст замены; объекты Label и TextBox IblRepl асе = new Label(); IblRepl ace.Content = "Re_place with;"; IblReplace.VerticalAlignment = VerticalAlignment.Center; IblRepl ace.Margin = new Thickness(12); gri d. Chi 1 dren. Addd bl Repl ace); Grid.SetRowdbl Repl ace, 1); Grid.SetColumndblReplace, 0);
клон Блокнота 459 txtboxReplасе = new TextBoxO; txtboxReplace.Margin = new Thickness(12): gr1d.Chi 1dren.Add(txtboxReplace); Grid.SetRow(txtboxReplace. 1); Grid.SetColumn(txtboxReplace, 1); // Флажок поиска с учетом регистра символов checkMatch = new CheckBoxO; checkMatch.Content = "Match _case"; checkMatch.VerticalAlignment = Vertical Alignment.Center; checkMatch.Margin = new Thickness(12); gri d.Ch i1dren.Add(checkMatch); Grid.SetRowtcheckMatch, 2); Grid.SetColumn(checkMatch. 0); // Направление поиска: GroupBox и два объекта RadioButton groupDi recti on = new GroupBoxO; groupDirecti on.Header = "Direction"; groupDirecti on.Margin = new Thickness(12); groupDirection.HorizontalAlignment = HorizontalAlignment.Left; grid.Chi 1dren.Add(groupDi rection); Grid.SetRow(groupDirection, 2); Grid.SetColumn(groupDirection, 1); StackPanel stack = new StackPanel(); stack.Orientation = Orientation.Horizontal; groupDirecti on.Content = stack; radiollp = new Radi oButton(); radiollp. Content = "JJp"; radiollp.Margin = new Thickness(6); stack .Children.Add(radiollp); radioDown = new RadioButton(); radioDown.Content = "_Down"; radioDown.Margin = new Thickness(6); stack.Children.Add(radioDown); // Создание объекта StackPanel для кнопок stack = new StackPanel 0; stack.Margin = new Thickness(6); gri d.Chi 1dren.Add(stack); Grid.SetRow(stack, 0); Grid.SetColumn(stack, 2); Grid.SetRowSpan(stack. 3); // Четыре кнопки btnFind = new ButtonO; btnFind.Content = " Find Next";
460 Глава 18. Клон Блокнота btnFind.Margin = new Thickness(6): btnFind.IsDefault = true; btnFind.Click += FindNextOnClick: stack.ChiIdren.Add(btnFind): btnReplace = new ButtonO: btnReplace.Content = "-Replace": btnReplace.Margin = new Thickness(6): btnReplace.Click += ReplaceOnClick; stack.Chi 1dren.Add(btnReplace): btnAl 1 = new ButtonO: btnAl1.Content = "Replace _A11"; btnAl1.Margin = new Thickness(6): btnAl1.Click += ReplaceAl1OnClick; stack.Children.Add(btnAll); Button btn = new ButtonO; btn.Content = "Cancel": btn.Margin = new Thickness(6): btn.IsCancel = true: btn.Click += CancelOnClick: stack.ChiIdren.Add(btn): txtboxFind. FocusO; } // Первые три кнопки становятся доступными // только после ввода искомого текста void FindTextBoxOnTextChanged(object sender. TextChangedEventArgs args) { TextBox txtbox = args.Source as TextBox: btnFind.IsEnabled = btnReplace.IsEnabled = btnAl1.IsEnabled = (txtbox.Text.Length > 0): } // Метод FindNextOnClick вызывает метод OnFindNext. // инициирующий событие FindNext void FindNextOnClick(object sender. RoutedEventArgs args) { OnFindNext(new EventArgsO); } protected virtual void OnFindNext(EventArgs args) { if (FindNext != null) FindNext(this, args); }
461 лон Блокнота // Метод ReplaceOnCl1ck вызывает метод OnReplace, // инициирующий событие Replace void ReplaceOnCl1ck(object sender, RoutedEventArgs args) { OnReplace(new EventArgsO); } protected virtual void OnReplace!EventArgs args) { if (Replace != null) Replace(this, args); } // Метод ReplaceAllOnClick вызывает метод OnReplaceAll, // инициирующий событие ReplaceAll void ReplaceA110nClick(object sender, RoutedEventArgs args) { OnReplaceAll(new EventArgsO): } protected virtual void OnReplaceAll(EventArgs args) { if (ReplaceAll != null) ReplaceAll(this, args); } // Кнопка Cancel просто закрывает диалоговое окно void CancelOnClick(object sender. RoutedEventArgs args) { Close(); } } } Три события, определяемые FindReplaceDialog, представляют собой стандарт- ные, типичные события .NET. Каждое событие ассоциируется с защищенным виртуальным методом, имя которого состоит из префикса On и имени события. On-методы отвечают за инициирование событий. В этой программе каждый On-метод вызывается обработчиком события Click кнопки, связанной с событием. On-методы ие являются обязательными, но они доступны для переопределения в классах, производных от FindReplaceDialog. Класс FindDialog, производный от FindReplaceDialog, просто скрывает неисполь- зуемые элементы. FindDialog.cs //-------------------------------------------- // FindDialog.cs (с) 2006 by Charles Petzold И--------------------------------------------- using System; using System.Windows; using System.Windows.Controls;
462 Глава 18. Клон Блокнота namespace Petzold.Notepadclone { class FindDialog : FindReplaceDialog { public FindDialog(Window owner): base(owner) { Title = "Find"; // Неиспользуемые элементы скрываются IblReplace.Visibility = Visibility.Col lapsed; txtboxReplace.Visibility = Visibility.Col lapsed; btnReplace.Visibility = Visibility.Collapsed: btnAll.Visibility = Visibility.Col lapsed; } } } Класс ReplaceDialog теоретически мог бы делать нечто большее, чем просто за- давать собственное свойство Title, но в данной программе он этим ограничивается. ReplaceDialog.cs //----------------------------------------------- // ReplaceDialog.cs (с) 2006 by Charles Petzold // using System: using System.Windows: using System.Windows.Controls; namespace Petzold.Notepadclone ( class ReplaceDialog : FindReplaceDialog { public ReplaceDialog(Window owner): base(owner) { Title = "Replace"; ) } } Класс FindReplaceDialog также использует следующее перечисление, определяю- щее направление поиска по тексту. Direction.cs namespace Petzold.NotepadClone { enum Direction ( Down, Up } }
Клон Блокнота 463 файл NotepadClone.Find.cs отвечает за создание трех объектов Menuitem для ме- ню Edit. Обработчики событий Click кнопок Find и Replace создают объекты типов FindDialog и ReplaceDialog соответственно и устанавливают обработчики для од- ного и более пользовательских событий, определяемых классом FindReplaceDialog. События оповещают программу о том, что пользователь щелкнул на одной из кнопок диалогового окна. NotepadClone.Find.cs //-------------------------------------------------- // Notepadclone.Find.cs (c) 2006 by Charles Petzold H--------------------------------------------------- using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input: namespace Petzold.Notepadclone { public partial class Notepadclone { string strFindWhat = "", strReplaceWith = ""; Stringcomparison strcomp = StringComparison.OrdinalIgnoreCase; Direction dirFind = Direction.Down: void AddFindMenuItems(Menuitem itemEdit) // Команда меню Find Menuitem itemFind = new MenuItemO: itemFind.Header = "_Find..."; itemFind.Command = ApplicationCommands.Find: itemEdit.Items.Add(itemFind): CommandBi ndi ngs.Add(new CommandBi ndi ng( Applicationcommands.Find, FindOnExecute, FindCanExecute)): // Для команды Find Next необходим пользовательский // объект RoutedUICommand InputGestureCollection coll = new InputGestureCollection(); coll.Add(new KeyGesture(Key.F3)): RoutedUICommand commFindNext = new RoutedUICommandC'Find _Next", "FindNext", GetTypeO, coll): Menuitem itemNext = new MenuItemO; itemNext.Command = commFindNext; itemEdit.Items.Add(itemNext): CommandBindings.Add( new CommandBinding(commFindNext, FindNextOnExecute, FindNextCanExecute)):
464 Глава 18. Клон Блокнота Menuitem itemReplace = new MenuItemO; itemReplace.Header = "-Replace..."; itemReplace.Command = ApplicationCommands.Repl ace; itemEdit.Items.Add(itemReplace); CommandBindi ngs.Add(new CommandBi nding( Appli cati onCommands.Repl ace, ReplaceOnExecute. FindCanExecute)); } // Метод CanExecute для команд Find и Replace void FindCanExecute(object sender, CanExecuteRoutedEventArgs args) args.CanExecute = (txtbox.Text.Length > 0 && OwnedWindows.Count == 0); } void FindNextCanExecute(object sender, CanExecuteRoutedEventArgs args) ( args.CanExecute = (txtbox.Text.Length > 0 && strFIndWhat.Length > 0); } // Обработчик для команды меню Find void FindOnExecute(object sender, ExecutedRoutedEventArgs args) { // Создание диалогового окна FindDialog dig = new FindDialog(this); // Инициализация свойств dlg.FindWhat = strFindWhat; dig.MatchCase = strcomp == StringComparison.Ordinal; dig.Direction = dirFind; // Установка обработчика события и вывод диалогового окна dlg.FindNext += FindDialogOnFindNext; dlg.Show(); } // Обработчик для команды меню Find Next // Если искомая строка еще не введена, клавиша F3 // вызывает диалоговое окно void FindNextOnExecute(object sender, ExecutedRoutedEventArgs args) { if (strFindWhat == null || strFindWhat.Length == 0) FindOnExecute(sender, args); else FindNext(); }
Клон Блокнота 465 // Обработчик для команды меню Replace void ReplaceOnExecute(object sender, ExecutedRoutedEventArgs args) { ReplaceDialog dig = new ReplaceDialog(this); dlg.FindWhat = strFindWhat; dlg.ReplaceWith = strReplaceWith; dig.MatchCase = strcomp == StringComparison.Ordinal; dig.Direction = dirFind; // Установка обработчиков событий dlg.FindNext += FindDialogOnFindNext; dig.Repl ace += ReplaceDialogOnReplace; dlg.ReplaceAll += ReplaceDialogOnReplaceAll; dlg.ShowO; } // Обработчик события, устанавливаемый для кнопки // "Find Next" диалогового окна Find/Replace void FindDialogOnFindNext(object sender, EventArgs args) { FindReplaceDialog dig = sender as FindReplaceDialog; // Получение свойств от диалогового окна strFindWhat = dlg.FindWhat; strcomp = dig.MatchCase ? StringComparison.Ordinal ; Stri ngCompari son.Ordi nalIgnoreCase; dirFind = dig.Di recti on; // Вызов FindNext проводит поиск FindNextO; } // Обработчик события, устанавливаемый для кнопки "Replace" // диалогового окна Replace void ReplaceDialogOnReplace(object sender, EventArgs args) { ReplaceDialog dig = sender as ReplaceDialog; // Получение свойств от диалогового окна strFindWhat = dlg.FindWhat; strReplaceWith = dlg.ReplaceWith; strcomp = dig.MatchCase ? StringComparison.Ordinal ; Stri ngCompari son.OrdinalIgnoreCase; if (strFindWhat.Equals(txtbox.SelectedText, strcomp)) txtbox.SeiectedText = strReplaceWith; FindNextO; }
466 Глава 18. Клон Блокнота // Обработчик события, устанавливаемый для кнопки "Replace АП" // диалогового окна Replace void ReplaceDialogOnReplaceAll(object sender, EventArgs args) ReplaceDialog dig = sender as ReplaceDialog; string stf = txtbox.Text: strFindWhat = dig J-indWhat; sr.rReplaceWith - d 1 g. Repl aceWIth: strcomp - dig.MatchCase ? StringComparison.Ordinal : Stг 1ngCompar ison.OrdinalIgnoreCase: int index = 0: while (index + strFindWhat.Length < str.Length) { index = str.IndexOf(strFindWhat, index, strcomp): 11 (index H -1) str - str.Removedndex, strFindWhat.Length); str - str. Insertdndex, strReplaceWith): index += strReplaceWith.Length; } else break: txtbox.Text = str; } // Обобщенный метод FindNext void FindNext() int indexstart. indexFind: // Переменная dirFind определяет начальную позицию // и направление поиска if (dirFind == Direction.Down) indexstart = txtbox.Selectionstart + txtbox.SeiectionLength: indexFind = txtbox.Text.IndexOf(strFindWhat, indexStart, strcomp); } else { indexStart = txtbox.SeiectionStart: indexFind = txtbox.Text.LastlndexOf(strFindWhat. indexStart, strcomp): } // Если IndexOf (или LastlndexOf) не возвращает -1, выделить // найденный текст. В противном случае вывести окно сообщения if (indexFind != -1)
клон Блокнота 467 { txtbox.Seiect(i ndexFi nd, strF1ndWhat.Length); txtbox.Focus(); } else MessageBox.Show("Cannot find \"" + strFindWhat + , Title, MessageBoxButton.OK, MessageBoxImage.Information); } } } Меню Format содержит две команды: Word Wrap и Font. В стандартной версии Блокнота системы Windows режим переноса (Word Wrap) может быть либо вклю- чен, либо выключен. В своем Клоне Блокнота я хотел предоставить все три ва- рианта (Textwrapping.NoWrap, TextWrapping.Wrap и TextWrapping.WrapWithOverflow) — хотя бы для тестирования логики печати класса PlainTextDocumentPaginator. Следующий класс WordWrapMenuItem является производным от Menuitem. Он представляет команду меню Format. Свойство Header содержит текст «Word Wrap», а коллекция Items — три команды подменю, соответствующие трем чле- нам перечисления TextWrapping. Обратите внимание: свойство Тад каждой коман- ды содержит одно из значений TextWrapping. WordWrapMenuItem.cs ц-------------------------------------------------- И WordWrapMenuItem.cs (с) 2006 by Charles Petzold И using System: using System.Windows: using System.Windows.Controls: using System.Windows.Controls.Primitives; namespace Petzold.Notepadclone { public class WordWrapMenuItem : Menuitem { // Регистрация зависимого свойства Wordwrap public static Dependencyproperty WordWrapProperty = DependencyProperty.RegisterC'WordWrap". typeof(TextWrapping), typeof(WordWrapMenuItem)); // Определение свойства Wordwrap public TextWrapping Wordwrap { set { SetValue(WordWrapProperty, value); } get { return (TextWrapping)GetValue(WordWrapProperty); } }
468 Глава 18. Клон Блокнота // Конструктор создает команду меню Word Wrap public WordWrapMenuItein() Header = "_Word Wrap": Menuitem item = new MenuItemO; item.Header = "_No Wrap"; item.Tag = Textwrapping.NoWrap; item.Click += MenuItemOnClick; Iterns.Add(item); item = new MenuItemO: item.Header = "_Wrap"; item.Tag = Textwrapping.Wrap; item.Click += MenuItemOnClick; Items.Add(item); item = new MenuItemO; item.Header = "Wrap with _0verflow"; item.Tag = TextWrapping.WrapWithOverflow; item.Click += MenuItemOnClick; Iterns.Add(item); // Пометка команды по текущему значению свойства Wordwrap protected override void OnSubmenuOpened(RoutedEventArgs args) base.OnSubmenuOpened(args); foreach (Menuitem item in Items) item.IsChecked - ((TextWrapping)itern.Tag == Wordwrap); } // Задание свойства Wordwrap по выбранной команде меню void MenultemOnClick(object sender, RoutedEventArgs args) { Wordwrap = (TextWrapping)(args.Source as Menuitem).Tag; } } } Класс определяет зависимое свойство WordWrapProperty, которое является ос- новой для открытого свойства Wordwrap. Класс обращается к свойству Wordwrap в своих двух обработчиках событий. Метод OnSubmenuOpened устанавливает по- метку команды, свойство Тад которой совпадает с текущим значением свойства Wordwrap. Метод MenuItemOnClick задает свойству Wordwrap текущее значение свойства Тад команды меню, па которой щелкнул пользователь. Обратите внимание: класс WordWrapMenuItem не содержит ссылок на TextBox. Если не считать открытого конструктора, открытого свойства Wordwrap и откры- того свойства WordWrapProperty, класс выглядит вполне «самодостаточным». Как же он взаимодействует со свойством Textwrapping объекта TextBox?
^онБлокнота 469 Как видно из следующего компонента класса Notepadclone, проблема решает- ся привязкой данных. flofpadClone.Format.es // Notepadclone.Format.cs (с) 2006 by Charles Petzold using Petzold.ChooseFont: using System: using System.Windows: using System.Windows.Controls: using System.Windows.Data; using System.Windows.Media: namespace Petzold.Notepadclone t public partial class Notepadclone { void AddFormatMenu(Menu menu) ( // Создание команды меню верхнего уровня Format Menuitem itemFormat = new MenuItemO; itemFormat.Header = "F_ormat"; menu.Items.Add(itemFormat); // Создание команды меню Word Wrap WordWrapMenuItem itemWrap = new WordWrapMenuItemO: itemFormat.Items.Add(itemWrap): // Привязка к свойству TextWrapping объекта TextBox Binding bind = new BindingO; bind.Path = new PropertyPath(TextBox.TextWrappingProperty); bi nd.Source = txtbox: bi nd.Mode = BindingMode.TwoWay; itemWrap.SetBinding(WordWrapMenuItem.WordWrapProperty, bind); // Создание команды меню Font Menuitem itemFont = new MenuItemO; itemFont.Header = "_Font..."; itemFont.Click += FontOnClick; i temFormat.Iterns.Add(itemFont); } // Обработчик события для команды Font void FontOnClick(object sender, RoutedEventArgs args) FontDialog dig = new FontDialogO; dig.Owner = this:
470 Глава 18. Клон Блокнота // Задание свойств TextBox в FontDialog dig.Typeface = new Typeface(txtbox.FontFamily. txtbox.Fontstyle, txtbox.FontWeight. txtbox.FontStretch); dig.FaceSize = txtbox.FontSize; if (dlq.ShowDialogO.GetValueOrDefaultO) { // Задание свойств FontDialog в TextBox txtbox.FontFamily = dig.Typeface.FontFamily; txtbox.FontSize = dig.FaceSize: txtbox.Fontstyle = dig.Typeface.Style; txtbox.FontWeight = dig.Typeface.Weight; txtbox.FontStretch = dig.Typeface.Stretch: } 1 } } После создания объекта WordWrapMenuItem и его включения в меню Format, программа создает объект Binding для привязки свойства TextWrapping объекта TextBox к свойству Wordwrap объекта WordWrapMenuItem: Binding bind = new BindingO; bind.Path = new PropertyPath(TextBox.TextWrappingProperty): bi nd.Source = LxtDox; bi nd.Mode = BindingMode.TwoWay; i temWrap.SetBinding(WordWrapMenuItem.WordWrapProperty. bi nd): Это не единственный способ определения привязки данных. В приведенном фрагменте TextBox рассматривается как источник данных, a WordWrapMenuItem - как приемник. Привязка действует в двустороннем режиме (TwoWay), чтобы изменения приемника также отражались в источнике. Тем не менее источник и приемник легко поменять местами: Binding bind = new BindingO; bind.Path = new PropertyPath(WordWrapMenuItem.WordWrapProperty); bi nd.Source = itemWrap: bi nd.Mode = BindingMode.TwoWay: txtbox.SetBinding(TextBox.TextWrappingProperty, bind); Обратите внимание: в последней команде теперь вызывается метод SetBinding объекта TextBox вместо WordWrapMenuItem. Самой простой частью меню Format является команда Font (если, конечно, у вас уже имеется класс FontDialog из предыдущей главы!) Обработчик события Click создает объект FontDialog, инициализирует свойства Typeface and FaceSize по данным TextBox, после чего обновляет свойства TextBox, если пользователь щелк- нет на кнопке ОК. Меню View получилось довольно тривиальным. Оно содержит единствен- ную команду Status Ваг, которая скрывает или отображает строку состояния про-
кпон Блокнота 471 граммы. Для этого ей необходимо лишь переключать свойство Visibility объекта StatusBar между состояниями Visibility .Visible и Visibility.Collapsed. NotepadClone.View.cs // NotepadClone.View.es (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; namespace Petzold.Notepadclone ( public partial class Notepadclone Menuitem itemStatus; void AddViewMenu(Menu menu) { // Создание команды меню верхнего уровня View Menuitem itemView = new MenuItemO; itemView.Header = "_View"; itemView.SubmenuOpened += ViewOnOpen; menu.Items.Add(itemView); // Создание команды Status Bar в меню View itemStatus = new MenuItemO; itemStatus.Header = "_Status Bar"; itemStatus.IsCheckable = true; itemStatus.Checked += StatusOnCheck; itemStatus.Unchecked += StatusOnCheck; itemView.Items.Add(itemStatus); } void ViewOnOpen(object sender. RoutedEventArgs args) ( itemStatus.IsChecked = (status.Visibility == Visibility.Visible); } void StatusOnCheck(object sender, RoutedEventArgs args) Menuitem item = sender as Menuitem; status.Visibility = item.IsChecked ? Visibility.Visible ; Visibility.Collapsed; } } }
472 Глава 18. Клон Блокнота Мы вышли на финишную прямую. Меню верхнего уровня завершается коман- доп Help, но я не буду заниматься ее реализацией. О том, как создать справочный файл, рассказано в главе 25. В этой версии программы меню Help содержит един- ственную команду «About Notepad Clone...», активизирующую класс AboutDialog. NotepadClone.Help.es //--------------------------------------------------- // NotepadClone.Help.es (с) 2006 by Charles Petzold //--------------------------------------------------- using System; using System.Reflection; using System.Windows; usi ng System.Wi ndows.Controls; namespace Petzold.Notepadclone { public partial class Notepadclone void AddHelpMenu(Menu menu) { Menu I tern itemHelp = new MenuItemO; itemHelp.Header = "_Help"; itemHelp.SubmenuOpened += ViewOnOpen; menu.Iterns.Add(itemHelp); Menuitem itemAbout = new MenuItemO; itemAbout.Header = "_About " + strAppTitle + itemAbout.Click += AboutOnClick; itemHelp.Iterns.Add(itemAbout); } void AboutOnClick(object sender. RoutedEventArgs args) { AboutDialog dig = new AboutDialog(this); dlg.ShowDialog(); } } Конструктор приведенного далее класса AboutDialog сначала обращается к сбор- ке для заполнения нескольких атрибутов, используемых для конструирования объектов TextBlock. В файле жестко закодирован только URL-адрес моего сайта, отображаемый с использованием текстового элемента Hyperlink. URL передается статическому методу Process.Start для запуска браузера. AboutDialog.cs //-------------------------------------------- // AboutDialog.cs (с) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Diagnostics; // для класса Process using System.Reflection;
Клон Блокнота 473 using System.Windows; using System.Windows.Controls; using System.Windows.Documents: using System.Windows.Media; namespace Petzold.Notepadclone { class AboutDIalog : Window ( public AboutD1alog(W1ndow owner) // Получение исполняемой сборки Assembly asmbly = Assembly.GetExecut1ngAssembly(): // Получение атрибута AssemblyTItle (имя программы) AssemblyTItleAttribute title = (AssemblyTI11 eAttribute)asmbly.GetCustomAttributes( typeof(AssemblyTitleAttrlbute). fa 1se)[0]: string strTItle = title.Title; // Получение атрибута AssemblyFIleVerslon AssemblyFIleVerslonAttrlbute version = (AssemblyFIleVersIonAttrlbute)asmbly.GetCustomAttr1butes( typeof(AssemblyF1leVersionAttrlbute). false)[0]: string strVerslon = version.Version.Substring^, 3); // Получение атрибута AssemblyCopyright AssemblyCopyrlghtAttrlbute copy = (AssemblyCopyr1ghtAttr1bute)asmbly.GetCustomAttr1butes( typeof(AssemblyCopyrlghtAttrlbute). talse)[0]; string strCopyrlght = copy.Copyright; // Стандартные свойства окна для диалоговых окон Title = "About " + strTItle: ShowInTaskbar = false: SizeToContent = SizeToContent.WidthAndHeight: ResizeMode = ResizeMode.NoResize: Left = owner.Left + 96; Top = owner.Top + 96; // Создание объекта StackPanel как содержимого окна StackPanel stackMaln = new StackPanel0: Content = stackMaln: // Создание объекта TextBlock с именем программы TextBlock txtblk = new TextBlockO; txtblk.Text = strTItle + " Version " + strVerslon; txtblk.FontFamily = new FontFamlly("Times New Roman"); txtblk.FontSize = 32; // 24 пункта txtblk.Fontstyle = Fontstyles.Italic: txtblk.Margin = new Th1ckness(24):
474 Глава 18. Клон Блокнота txtblk.HorizontalAlignment = HorizontalAlignment.Center: stackMain.Children.Add(txtblk); // Создание объекта TextBlock с информацией об авторских правах txtblk = new TextBlockO; txtblk.Text = strCopyright: txtblk.FontSize =20; // 15 пунктов txtblk.HorizontalAlignment = Horizontal Alignment.Center; stackMain.Chi 1dren.Add(txtblk); // Создание объекта TextBlock co ссылкой на сайт Run run = new RunCwww.charlespetzold.com"): Hyperlink link = new Hyperlink(run); link.Click += LinkOnClick; txtblk = new TextBlock(link); txtblk.FontSize = 20; txtblk.HorizontalAlignment = HorizontalAlignment.Center: stackMain.Chi 1dren.Add(txtblk); // Создание кнопки OK Button btn = new ButtonO; btn.Content = "OK": btn.IsDefault = true: btn.IsCancel = true; btn.HorizontalAlignment = HorizontalAlignment.Center: btn.Margin = new Thickness(24): btn.Click += OkOnClick; stackMa i n.Chi 1dren.Add(btn): btn.FocusO: } // Обработчики событий void LinkOnClick(object sender, RoutedEventArgs args) { Process.Start("http://www.charlespetzold.com"); } void OkOnClick(object sender, RoutedEventArgs args) { DialogResult = true: } } У кнопки OK свойства IsDefault и IsCancel равны true, чтобы пользователь мог закрыть диалоговое окно как клавишей Enter, так и клавишей Escape. Этот файл завершает проект Клон Блокнота. Как было сказано ранее, в начале главы 20 мы вернемся к этому проекту, добавим в него пару файлов и превра- тим в служебную программу XAML Cruncher. В следующей главе я представлю читателю язык XAML и постараюсь наглядно показать, что программа XAML Cruncher — весьма полезный инструмент.
XAML Перед вам синтаксически правильный фрагмент кода XML: <Button Foreground="L1ghtSeaGreen" FontS1ze="24pt"> Hello, XAML! </Button> Эти три строки образуют один элемент XML: начальный тег, конечный тег и содержимое, заключенное между двумя тегами. Элемент относится к типу Button. Начальный тег включает определения двух атрибутов с именами Fore- ground и FontSize. Атрибутам присвоены значения, которые по правилам XML должны быть заключены в апострофы или кавычки. Между начальным и конеч- ным тегом располагается содержимое элемента; в данном примере это символь- ные данные (в терминологии XML). Язык XML проектировался как универсальный язык разметки, применяе- мый в разнообразных областях. Одной из таких областей является язык XAML (Extensible Application Markup Language) — вспомогательный программный ин- терфейс WPF. Как вы, вероятно, догадались, этот фрагмент XML также являет- ся синтаксически правильным фрагментом XAML. Button — класс, определяемый в пространстве имен System.Windows.Controls, a Foreground и FontSize — свойства этого класса. Строка «Hello, XAML!» — текст, который обычно задается свойст- ву Content объекта Button. Язык XAML проектировался в основном для создания и инициализации объ- ектов. Приведенный ранее фрагмент XAML примерно соответствует следующе- му (более длинному) коду С#: Button btn = new ButtonO; btn.Foreground = Brushes.LIghtSeaGreen; btn.FontSize = 32: ,, btn.Content = "Hello, XAML!" Обратите внимание: XAML не требует, чтобы имя LightSeaGreen было явно объявлено членом класса Brushes, а строка «24рЬ> принимается как представление 24 пунктов. Один типографский пункт составляет 1/72 дюйма, поэтому 24 пунк- та соответствуют 32 аппаратно-независимым единицам.
476 Глава 19, XAML Хотя код XML часто бывает довольно объемистым (a XAML в некоторых от- ношениях дополнительно увеличивает объем кода), XAML нагляднее эквива- лентного процедурного кода. Макет окна программы часто представляет собой иерархию панелей, элемен- тов управления и других интерфейсных элементов. В XAML иерархия отража- ется при помощи вложенных элементов: <StackPanel> <Button Foreground="LightSeaGreen" FontSize="24pt"> Hello. XAML! </Button> <E11 Ipse Fill="Brown" Width=,,200,‘ Height=,,100" /> <Button> <Image Source="http://www.charlespetzold.com/PetzoldTattoo Jpg" Stretch="None" /> </Button> </StackPanel> Объект StackPanel из этого фрагмента содержит три дочерних объекта: Button, Ellipse и снова Button. Первый объект Button содержит текст, а содержимое второго представлено объектом Image. Поскольку объекты Ellipse и Image не имеют содер- жимого, для записи элементов можно воспользоваться специальным синтаксисом XML, при котором конечный тег заменяется косой чертой перед закрывающей угловой скобкой начального тега. Также обратите внимание, что для задания ат- рибуту Stretch элемента Image значения из перечисляемого типа Stretch достаточ- но указать имя члена перечисления. Файл XAML часто может заменить целый конструктор класса, производно- го от Window, — обычно именно в этой части класса выполняется макетирование и устанавливаются обработчики событий. Сами обработчики должны быть напи- саны на процедурном языке (таком, как С#). Но если обработчик можно заме- нить привязкой данных, обычно привязка находит прямое отражение в XAML XAML отделяет визуальное оформление приложения от его функционально- сти. Благодаря такому разделению дизайнер может создать эффектный пользо- вательский интерфейс, работая с XAML-файлом, пока программист занимается взаимодействием элементов управления и интерфейсных элементов. Также ста- ли появляться средства проектирования, генерирующие код XAML. Visual Studio содержит собственный встроенный дизайнер, генерирующий код XAML. Конечно, дизайнер, генерирующий XML, намного полезнее дизайнера, генерирующего код С#, как это было с дизайнером Visual Studio для Windows Forms. Дизайнер, сгенерировавший процедурный код, позднее должен его прочи- тать, поэтому код часто должен иметь жестко фиксированный формат. По этой причине программисту не разрешается редактировать его. Напротив, язык XML проектировался для удобства редактирования как человеком, так и компьюте- ром. Если редактор сохраняет XAML в синтаксически правильном состоянии, проблем быть не должно. Несмотря на наличие дизайнеров, генерирующих код XAML, вам как про- граммисту будет весьма полезно освоить синтаксис XAML, а лучший способ
XAML_477 изучения, как известно, — практика. На мой взгляд, каждый программист WPF должен хорошо разбираться в XAML и уметь писать XAML-код вручную — сей- час я покажу, как это делается. фрагменты XAML, приводившиеся до настоящего момента, возможны лишь в контексте более крупного документа XAML; сами ио себе существовать они не Могут. Возникает некоторая неоднозначность. Что такое элемент Button — кноп- ка? Но какая? Кнопка в диалоговом окне? Канцелярская кнопка? Дверной зво- нок? Если в двух документах XML одно имя элемента используется для разных целей, несомненно, эти два документа должны каким-то образом различаться. Для этой цели в XML были созданы пространства имен. Документ XAML, созданный программистом WPF, использует другое пространство имен, нежели документы XML, созданные производителем канцелярских кнопок. Пространство имен XML по умолчанию объявляется в документах при по- мощи атрибута xmlns. Пространство имен относится к элементу, в котором нахо- дится его объявление, а также к его дочерним элементам. Имена пространств имен XML должны быть уникальными; в качестве имен очень часто используют- ся URL. Для кода XAML, используемого в программах WPF, используется WPF http://schemas.mi crosoft.com/winfx/2006/xaml/presentation He торопитесь открывать этот адрес в браузере — там ничего нет. Это просто имя пространства имен, которое было создано компанией Microsoft для одно- значной идентификации элементов XAML (таких, как Button, StackPanel и Image). Чтобы превратить фрагмент кода XML, приведенный в начале главы, в XAML, следует добавить в него атрибут xmlns и соответствующее пространство имен: <Button xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation” Foreground="LightSeaGreen" FontSize="24pt"> Hello, XAML! </Button> Созданный код XAML можно сохранить в небольшом файле — например, созданном в Блокноте или Клоне Блокнота. Вероятно, в начало файла стоит включить комментарий XML с описанием файла. Сохраните полученный файл на своем жестком диске. XamlButton.xaml <! -- ============================================= XamlButton.xaml (с) 2006 by Charles Petzold <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Foreground="LightSeaGreen" FontSize="24pt"> Hello, XAML! </Button> Если в системе установлены расширения WinFx для .NET, а также в системе Microsoft Vista, полученный файл можно запустить как программу (то есть Двойным щелчком на имени файла в Проводнике). Также можно запустить его
478 Глава 19. XAML в командной строке. На экране появляется окно Internet Explorer, а кнопка за- полнит основную площадь клиентской области, за исключением пары навигаци- онных кнопок под строкой адреса. Навигационные кнопки недоступны, потому что переходить с этой страницы некуда. (В Microsoft Vista навигационные кноп- ки не отображаются в клиентской области; их функциональность обеспечивает- ся собственными навигационными кнопками Internet Explorer.) Такие файлы, как XamlButton.xaml, называются автономными XAML-файлами. Расширение файла .xaml ассоциируется с программой PresentationHost.exe. Запуск файла приводит к запуску PresentationHost.exe; эта программа отвечает за создание объекта типа Раде (класс, производный от FrameworkElement, но отчасти похо- жий на Window). Объект отображается в Internet Explorer. Программа Presentation- Host.exe также преобразует загруженный код XAML в объект Button и задает его свойству Content объекта Раде. Если код XAML содержит ошибку, Internet Explorer выведет соответствую- щее сообщение. Кнопка вызова дополнительной информации в Internet Explorer сообщает о присутствии PresentationHost.exe и выводит результаты трассировки стека. Наряду с множеством других методов в трассировке присутствует ста- тический метод XamlReader.Load из пространства имен System.Windows.Markup. Именно этот метод преобразует XAML в объекты; вскоре я покажу, как исполь- зовать его. Помимо запуска XamlButton.xaml с жесткого диска, файл также можно размес- тить на веб-сайте и запустить его с сайта. Возможно, для этого придется зареги- стрировать тип MIME для расширения .xaml. На некоторых серверах для этого достаточно включить следующую строку в файл .htaccess: AddType application/xaml+xml xaml А вот еще один пример автономного XAML-файла, который создает объект StackPanel с тремя дочерними объектами — Button, Ellipse и ListBox. XamlStackPanel.xaml <।_ _ ================================================= XamlStackPanel.xaml (c) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation'^ <Button HorizontalAlignment="Center" Margin="24"> Hello, XAML! </Button> <E1 lipse Width="200" Height=,,100" Margin="24" Stroke="Red" StrokeThickness="10" /> <ListBox Width="100" Height="100" Margin="24"> <Li stBoxItem>Sunday</Li stBoxItem> <Li stBoxItem>Monday</Li stBoxItem> <ListBoxItem>Tuesday</Li stBoxItem> <ListBoxItem>Wednesday</ListBoxItem>
479 XAML <ListBoxItem>Thursday</ListBoxItem> <ListBoxItem>Friday</ListBoxItem> <Li stBoxItem>Saturday</Li stBoxItem> </ListBox> </StackPanel> XML-файл может иметь только один корневой элемент. В данном файле кор- невым элементом является StackPanel. Между начальным и конечным тегами размещается содержимое StackPanel — три дочерних элемента. Элемент Button очень похож на то, что мы уже видели. Элемент Ellipse содержит пять атрибутов, соответствующих свойствам класса Ellipse, но не имеет содержимого, поэтому в его оформлении используется синтаксис пустого элемента. Элемент ListBox содержит семь дочерних элементов ListBoxItem. Содержимое каждого элемента ListBoxItem представляет собой текстовую строку. Обычно XAML-файлы представляют целое дерево элементов. В процессе за- грузки XAML-файла PresentationHost.exe не только создает и инициализирует все элементы дерева, но и объединяет их в визуальное дерево. При запуске автономных XAML-файлов в строке заголовка Internet Explorer отображается полное имя файла. Вероятно, в реальном приложении вы бы пред- почли сами задать текст, который там отображается. Для этого можно назначить корневым элементом Раде, задать свойство WindowTitle и назначить StackPanel до- черним элементом Раде, как в следующем автономном XAML-файле. XamlPage.xaml <! - - =========================================== XamlPage.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" WindowTitle="Xaml Page"> <StackPanel> <Button HorizontalAlignment="Center" Margin="24"> Hello, XAML! </Button> <E11 ipse Width="200" Height="100" Margin="24" Stroke="Red" StrokeThickness="10" /> <ListBox Width="100" Height="100" Margin="24"> <Li stBoxItem>Sunday</ListBoxItem> <ListBoxltem>Monday</ListBoxItem> <Li stBoxItem>Tuesday</Li stBoxItem> <Li stBoxItem>Wednesday</Li stBoxItem> <Li stBoxItem>Thursday</Li stBoxItem> <ListBoxItem>Friday</ListBoxItem>
480 Глава 19.J(am, <Li st Box 1t em>Sa t u relay </L1 stBox!tem> </ListBox> </StackPanel> </Page> Возможно, вам захочется опробовать автономный XAML-файл с корневым элементом Window. С этим ничего не выйдет, потому что PresentationHost.exe пытается сделать корневой элемент дочерним по отношению к другому объекту, а объект Window им быть не может. Корневой элемент автономного XAML-фай- ла может относиться к любому типу, производному от FrameworkElement, кро- ме Window. Предположим, у вас имеется программа С#, определяющая строковую пере- менную с именем strXaml. Переменная содержит небольшой, но законченный до- кумент XAML: string strXaml = "<Button xmlns='http://schemas.microsoft.com/winfx/2006/presentation'" + " Foreground='LightSeaGreen' FontSize='24pt,>" + " Click me!” + "</Button>"; Чтобы строка лучше читалась, я использовал апострофы в атрибутах вместо кавычек. Можно ли написать программу, которая разбирает эту строку, создает и инициализирует объект Button? Конечно, для этого нам придется применять рефлексию и руководствоваться некоторыми допущениями по отношению к дан- ным, используемым для задания свойств Foreground и FontSize. Написать парсер такого рода вполне реально, но он уже существует. Пространство имен System. Windows.Markup содержит класс XamIReader со статическим методом Load, который разбирает код XAML и превращает его в инициализированный объект. (А ста- тический метод XamlWriter.Save работает в обратном направлении — он генериру- ет код XAML по объектам.) Программы WPF используют метод XamIReader.Load для преобразования бло- ка XAML в объект. Если корневой элемент XAML содержит дочерние элемен- ты, последние тоже преобразуются и объединяются в визуальное дерево, опи- санное иерархией XAML. Конечно, для использования XamIReader.Load программа должна содержать директиву using для пространства имен System.Windows.Markup, но кроме нее так- же понадобится ссылка на сборку System.Xml.dll с классами, необходимыми для XamIReader.Load. К сожалению, напрямую передать строковый аргумент методу XamIReader.Load невозможно; в противном случае можно было бы передать код XAML методу и преобразовать результат в объект нужного типа: Button btn = (Button) XamlReader.Load(strXaml): // He получится! Методу XamIReader.Load передается либо объект Stream, либо объект XmlReader. В одном из решений, попадавшихся мне, для выполнения операции использует- ся объект Memorystream (в программу необходимо включить директиву using для
481 XAML постранства имен System.10). Объект StreamWriter записывает строку в Memory- Stream, a Memorystream передается XamIReader.Load: Memorystream memory = new MemoryStream(strXaml.Length); StreamWriter writer = new StreamWriter(memory): writer.Write(strXaml); writer.FlushO; memory.Seek(0, SeekOri gi n.Begi n); object obj = XamlReader.Load(memory): А вот чуть более элегантное решение, требующее директив using для System.Xml и System.Ю: StringReader strreader = new StringReader(strXaml): XmlTextReader xmlreader = new XmlTextReader(strreader): object obj = XamlReader.Load(xmlreader); Впрочем, можно обойтись всего одной (правда, почти нечитаемой) командой: object obj = XamlReader.Load(new XmlTextReader(new StringReader(strXaml))); Следующая программа определяет строку strXaml так, как показано выше, пре- образует короткий документ XAML в объект и задает его свойству Content окна. LoadEmbeddedXaml.cs // 11 LoadEmbeddedXaml.es (c) 2006 by Charles Petzold // using System: using System.10: using System.Windows: using System.Windows.Controls: using System.Windows.Markup: using System.Xml; namespace Petzold.LoadEmbeddedXaml public class LoadEmbeddedXaml : Window [STAThread] public static void MainO Application app = new ApplicationO: app.Run(new LoadEmbeddedXaml()): public LoadEmbeddedXaml() Title = "Load Embedded Xaml": string strXaml = "<Button xmlns='http://schemas.microsoft.com/" +
482 Глава 19. xaml "winfx/2006/xaml/presentation'" + " Foreground='LightSeaGreen' FontSize='24pt,>" + " Click me!" + "</Button>": StringReader strreader = new StringReader(strXaml); XmlTextReader xml reader = new XmlTextReader(strreader); object obj = XamlReader.Load(xmlreader); Content = obj; } } } Поскольку программа «знает», что объект, возвращаемый XamIReader.Load, в действительности является объектом Button, она может преобразовать его к ти- пу Button: Button btn = (Button) XamlReader.Load(xmlreader); Затем программа может установить обработчик события, если он присутст- вует в коде: btn.Click += ButtonOnClick; С таким объектом Button можно сделать то же, как если бы в программе при- сутствовал явный код его создания и инициализации. Конечно, определять код XAML в виде строковой переменной неудобно. Вероятно, для преобразования блока XAML в объект на стадии выполнения лучше воспользоваться другим способом — загрузкой ресурса, хранящегося в ис- полняемом файле программы. Как обычно, сначала следует создать пустой проект — назовем его Load- XamlResource. Включите ссылку на сборку System.Xml наряду с другими сборка- ми WPF. Выполните команду Add New Item из меню Project (или щелкните правой кнопкой мыши на имени проекта и выберите команду в контекстном меню). Выберите шаблон XML File и имя файла LoadXamlResource.xmL (Если использовать расширение .xaml вместо .xml, Visual Studio попытается загрузить дизайнера XML, а также сделает некоторые предположения относительно файла, которые пока не соответствуют действительности.) Далее приводится код XML файла. LoadXamIResource.xml <1__ ================================================== LoadXamIResource.xml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <Button Name="MyButton" Hori zonta1Ali gnment="Center" Margin="24"> Hello. XAML! </Button>
483 XAML <E1 llpse W1dth="200" He1ght="100" Marg1n="24" Stroke="Red" StrokeTh1ckness="10" /> <L1stBox W1dth="100" He1ght="100" Marg1n="24"> <L1stBox!tem>Sunday</L1 stBoxItem> <L1stBoxItem>Monday</L1stBoxItem> <L1stBoxItem>Tuesday</L1stBoxItem> <L1stBoxItem>Wednesday</L1stBoxItem> <L1stBoxItem>Thursday</L1stBoxItem> <L1stBoxItem>Fr1 day</L1stBoxItem> <Li stBoxItem>Saturday</L1stBox!tem> </L1stBox> </StackPanel> Конструктор создает объект Uri для ресурса XML и использует статическое свойство Application.GetResourceStream для получения объекта StreamResourcelnfo. Объект включает свойство Stream, которое возвращает объект Stream для ресур- са. Последний становится аргументом XamIReader.Load, а объект, возвращаемый свойством (объект типа StackPanel), задается свойству Content окна. После того как объект, преобразованный в XAML, становится частью визу- ального дерева окна, становится возможно использовать метод FindName для по- иска в дереве элемента с заданным именем. Далее программа может установить обработчик события или сделать с элементом что-то еще. Вероятно, это самый прямолинейный способ подключения обработчиков событий к коду XAML во время выполнения. А вот небольшая вариация на исходную тему: проект называется LoadXaml- Window, и как и в предыдущем проекте, свойству Build Action XML-файла должно быть задано значение Resource. LoadXamlWindow.xml <! -- ================================================ LoadXamlWindow.xml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" T1tle="Load Xaml Window" S1zeToContent="W1dthAndHe1ght" Res1zeMode="CanM1n1m1ze"> <StackPanel> <Button Horizontal Al1gnment="Center" Marg1n="24"> Hello, XAML! </Button>
484________________________________________ Глава 19. XAML -----— <Е11 Ipse W1dth="200'‘ He1ght="100" Margln^'24" Stroke=,,Red" StrokeTh1ckness="10" /> <L1stBox W1dth="100" He1ght="100" Marg1n=,,24"> <L1stBox!tem>Sunday</L1stBox!tem> <L1 stBoxItem>Monday</L1stBoxItem> <ListBoxItem>Tuesday</L1stBoxItem> <Li stBoxItem>Wednesday</L1stBoxItem> <L1stBox!tem>Thursday</ListBoxItem> <L1stBoxItem>Fr1day</L1stBox!tem> <L1stBoxItem>Saturday</L1stBoxItem> </L1stBox> </StackPanel> </W1ndow> В этом коде XAML корневым элементом является Window. Обратите внимание: в число атрибутов начального тега Window входят Title, SizeToContent и ResizeMode. Двум последним атрибутам задаются значения из перечислений, ассоциирован- ных с каждым свойством. Корневой элемент Window запрещен для автономных XAML-файлов. К сча- стью, следующая программа знает, что ресурс XAML представляет объект Win- dow, поэтому она не пытается наследовать от Window или напрямую создавать объект этого класса. LoadXamlWindow.cs //----------------------------------------------- // LoadXamlW1ndow.cs (с) 2006 by Charles Petzold //----------------------------------------------- using System; using System.10; using System.Windows; using System.Windows.Controls; using System.Windows.Markup; namespace Petzold.LoadXamlWIndow public class LoadXamlWindow [STAThread] public static void MainO Application app = new ApplicationO; Uri uri = new Uri(“pack://application:..,/LoadXamlWindow.xml");
XAML__485 Stream stream = Application.GetResourceStream(uri).Stream; Window win = XamlReader.Load(stream) as Window: win.AddHandler(Button.Cl 1ckEvent. new RoutedEventHandler(ButtonOnCllck)); app.Run(wln); } static void ButtonOnClick(object sender, RoutedEventArgs args) { MessageBox.ShowC’The button labeled + (args.Source as Button).Content + "' has been clicked"); } } } Метод Main создает объект Application, загружает XAML и преобразует возвра- щаемое значение XamIReader.Load в объект Window. Для установки обработчика события Click используется не поиск кнопки в визуальном дереве, а вызов мето- да AddHandler окна. Наконец, метод Main передает объект Window методу Run объ- екта Application. Следующая программа открывает диалоговое окно Open File, в котором поль- зователь выбирает XAML-файл на диске. При помощи этой программы можно загрузить любой из XAML-файлов, приводившихся ранее в этой главе, в том числе и файл с расширением .xml. LoadXamlFile.cs //-------------------------------------- // LoadXamlF11e.cs (с) 2006 by Charles Petzold И--------------------------------------- using Microsoft.Win32; using System; using System.10; using System.Windows; using System.Windows.Controls; using System.Windows.Markup; using System.Xml; namespace Petzold.LoadXamlFile { public class LoadXamlFile : Window { Frame frame; [STAThread] public static void MainO Application app = new ApplicationO; app.Run(new LoadXamlF11e());
486 Глава 19J(aM| public LoadXamlF11e() { Title = "Load XAML File"; DockPanel dock = new DockPanel 0; Content = dock; // Создание кнопки для диалогового окна Open File Button btn = new ButtonO: btn.Content = "Open File..."; btn.Margin = new Th1ckness(12): btn.HorizontalAlignment = HorizontalAlIgnment.Left; btn.Click += ButtonOnClick: dock.Children.Add(btn); DockPanel.SetDock(btn, Dock.Top); // Создание объекта Frame для размещения загруженного кода frame = new FrameO; dock.Chi 1dren.Add(frame); } void ButtonOnClick(object sender. RoutedEventArgs args) { OpenFileDialog dig = new OpenFileDialog(): dig.Filter = "XAML Files (*.xaml)|*.xaml|A11 files (*.*))*.*"; if ((bool)dlg.ShowDialogO) { try { // Чтение файла с использованием XmlTextReader XmlTextReader xml reader = new XmlTextReader(dlg.Fl 1 eName); // Преобразование XAML в объект object obj = XamIReader.Load(xmlreader); // Если это объект Window, вызвать Show If (obj Is Window) { Window win = obj as Window; win.Owner = this: wln.ShowO; } // В противном случае задать содержимым Frame else frame.Content = obj; } catch (Exception exc)
487 XAML_____________________________________________________ { MessageBox.Show(exc.Message, Title); } } } } } Как видно из метода ButtonOnClick, получение имени файла от OpenFileDialog немного упрощает загрузку XAML по сравнению с программами, использую- щими XAML в виде ресурса. Имя файла передается конструктору XmlTextReader, а созданный объект принимается методом XamIReader.Load. Метод содержит специальную логику на тот случай, если объект, возвращае- мый XamIReader.Load, относится к типу Window. В этом случае свойству Owner объекта Window задается текущий объект окна, после чего для загруженного окна вызывается метод Show, как если бы оно было немодальным диалоговым окном. Я добавил код задания свойства Owner после того, как обнаружил, что после закрытия главного окна приложения загруженное окно продолжало суще- ствовать, мешая закрытию приложения. Мне такое поведение показалось непра- вильным. Возможно и другое решение — задать свойство ShutdownMode объекта Application равным ShutdownMode.OnMainWindowClose; именно такой подход исполь- зован в программе XAML Cruncher, описанной в следующей главе. Мы рассмотрели пару способов загрузки дерева элементов XAML во время выполнения программы, а также получили представление о том, как код, загру- зивший XAML, может искать в дереве нужные элементы и устанавливать для него обработчики событий. Тем не менее в реальных приложениях код XAML гораздо чаще компилиру- ется вместе с остальным исходным кодом. Конечно, такой вариант более эффек- тивен; к тому же, компиляция XAML позволяет то, что невозможно сделать в ав- тономном XAML — например, задать имя обработчика события прямо в XAML. Обычно обработчик находится в файле с процедурным кодом, но код C# также можно внедрить прямо в XAML, а это возможно только при компиляции XAML в составе проекта. Как правило, проект содержит один файл XAML для каждой страницы или окна приложения (включая диалоговые окна), а с каждым фай- лом XAML ассоциируется кодовый файл (часто называемый файлом программ- ной логики). Впрочем, в своих проектах вы можете использовать XAML в той мере, в какой считаете нужным. Во всем коде XAML, приводившемся ранее, использовались классы и свой- ства, входящие в WPF. Однако XAML не является специализированным язы- ком разметки WPF. Правильнее считать WPF одной из возможных областей применения XAML. Язык может использоваться и в других архитектурах, не имеющих отношения к WPF. Спецификация XAML определяет ряд элементов и атрибутов, которые могут использоваться в любых приложениях, включая WPF. Эти элементы и атрибу- ты ассоциируются с пространством имен XML, отличным от пространства имен WPF, и для использованиях элементов и атрибутов, специфических для XAML,
488 Глава 19. XAML необходимо включить в XAML второе объявление пространства имен со сле- дующим URL: http://schemas.mlcrosoft.com/winfx/2006/xaml URL совпадает с тем, который используется для WPF, за исключением до- полнительного уровня presentation. Объявление пространства имен WPF будет встречаться во всех файлах XAML, приводимых в книге: xmlns="httр://schemas.mi crosoft.com/winfx/2006/xaml/presentation" Пространство имен XAML-специфических элементов и атрибутов обычно объявляется с префиксом х: xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml" Конечно, вы можете выбрать любой префикс по своему усмотрению (при ус- ловии, что он не начинается с букв XML), но префикс х стал неписаным стан- дартом в большинстве файлов XAML. В этой главе будут приведены примеры атрибута Class и элемента Code, при- надлежащих пространству имен XAML. Так как пространство имен XAML обыч- но ассоциируется с префиксом х, атрибут Class и элемент Code обычно имеют вид x:Class и x:Code; именно так я буду ссылаться на них. Атрибут x:Class может присутствовать только в корневом элементе файла XAML и только в коде XAML, компилируемом в составе проекта. Он не может использоваться в автономном XAML-файле или коде XAML, загружаемом про- граммой на стадии выполнения. Атрибут x:Class выглядит примерно так: x:Class="MyNamespace.MyClassName" Очень часто атрибут x:Class встречается в корневом элементе Window, поэто- му общая структура файла XAML может выглядеть примерно так: <Ы1ndow xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml" x:Class="MyNamespace.MyClassName" </W1ndow> Упоминаемое пространство имен MyNamespace является пространством .NET, ассоциированным с проектом приложения. Очепь часто на C# пишется соответ- ствующий класс Window с тем же пространством имен и именем класса, который определяется с ключевым словом partial: public namespace MyNamespace { public partial class MyClassName: Window { }
489 XAML Это и есть файл программной логики. Такие файлы содержат код (чаще все- го обработчики событий, но иногда и код инициализации), обеспечивающий ра- боту элементов из файла XAML. Фактически файл XAML и файл программной логики являются составляющими одного класса, который часто является клас- сом типа Window. Как обычно, начнем с пустого проекта. Назовем его CompileXamlWindow. Проект состоит из двух файлов: файла XAML с именем CompileXamlWindow.xaml и файла C# с именем CompileXamlWindow.cs. Оба файла составляют один класс с полным именем Petzold.CompileXamlWindow.CompileXamlWindow. Начнем с создания файла XAML. В пустом проекте создайте новый файл XML с именем CompileXamlWindow.xaml. В левом нижнем углу окна исходного кода вы- берите вкладку Xaml вместо вкладки Design. В свойствах файла CompileXamlWindow.xaml значение Build Action должно быть равно Раде. Ранее в проектах LoadXamlResource и LoadXamlWindow я предла- гал использовать для файлов XAML расширение .xml, а теперь переключился на расширение .xaml. Но на самом деле важно не расширение, а значение Build Action. В предыдущем проекте код XAML оформлялся в виде ресурса. В текущем про- екте файл должен быть откомпилирован; именно для этого свойству Build Action задается значение Раде. Файл CompileXamlWindow.xaml отчасти похож на LoadXamlWindow.xml. Первое серьезное различие заключается в том, что файл содержит объявление второго пространства имен для префикса х, а в корневом элемента присутствует атри- бут x:Class. Мы определяем класс, производный от Window, с полным именем Petzold. CompileXamlWindow.CompileXamlWindow. CompileXamlWindow.xaml < I _ _ ==================================================== CompileXamlWindow.xaml (c) 2006 by Charles Petzold <W1 ndow xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentatlon" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml" x:Class="Petzold.Compl1eXamlWIndow.Compl1eXamlWIndow" T1tle="Comp11e XAML Window" SIzeToContent="W1dthAndHelght" ReslzeMode="CanM1nlmlze"> <StackPanel> <Button Horizontal Al1gnment="Center" Marg1n="24" Cl1ck="ButtonOnCl1ck"> Click the Button </Button> <E111pse Name="el1ps" W1dth="200" He1ght="100"
490 Глава 19. XAML Marg1n="24" Stroke="Black'7> <L1stBox Name="Istbox" W1dth="150" He1ght="150" Margin="24" SeiectionChanged="LIstBoxOnSelection" /> </StackPanel> </W1ndow> Этот документ XAML определяет класс, который в синтаксисе C# выглядит примерно так: namespace Petzo1d.Compi1eXamlWi ndow { public partial class CompileXamlWindow: Window { } } Ключевое слово partial означает, что класс CompileXamlWindow содержит до- полнительный код, хранящийся в другом файле — в данном случае в файле про- граммной логики С#. Также обратите внимание, что элемент XAML кнопки содержит атрибут со- бытия Click и задает ему обработчик с именем ButtonOnClick. Где определяется этот обработчик? В коде C# класса CompileXamlWindow. Объекту ListBox также по- требуется обработчик события SelectionChanged. Как элемент Ellipse, так и элемент ListBox содержат атрибут Name со значения- ми elips и Istbox соответственно. Ранее вы видели, как программа ищет эти эле- менты в дереве методом FindName. При компиляции XAML в проекте атрибуты Name играют исключительно важную роль. Они становятся полями класса, по- этому класс, созданный на базе XAML в процессе компиляции, больше похож на следующий: namespace Petzo1d.Compi1eXamlWIndow { public partial class CompileXamlWindow: Window { Ellipse ellps; ListBox Istbox: } } В части класса CompileXamlWindow, написанной на С#, можно ссылаться на эти поля напрямую. Далее приводится файл программной логики с остальным кодом класса CompileXamlWindow.
491 XAML CompileXamlWindow.es // Comp11eXamlW1ndow.es (с) 2006 by Charles Petzold II-------------------------------------------------- using System: using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzol d.Compj1eXamlWj ndow public partial class ComplleXamlWindow : Window { [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new Compi 1 eXamlWindowO); public Compi 1 eXamlWIndow() { // Обязательный вызов метода для подключения // обработчиков событий и инициализации полей Initial1zeComponent(); // Заполнение объекта ListBox именами кистей foreach (Propertyinfo prop in typeof(Brushes).GetPropertiesO) Istbox.Items.Add(prop.Name); } // Обработчик события кнопки просто вызывает окно сообщения void ButtonOnClick(object sender, RoutedEventArgs args) Button btn = sender as Button; MessageBox.ShowC'The button labled '" + btn.Content + "' has been clicked."); } // Обработчик события ListBox изменяет // свойство Fill объекта Ellipse void ListBoxOnSelection(object sender, SelectionChangedEventArgs args) { ListBox Istbox = sender as ListBox; string stritem = Istbox.Selectedltem as string;
492 Глава 19. XAML Propertyinfo prop = typeoflBrushes).GetProperty(strltem); elips.Fill = (Brush)prop.GetValue(null. null): } } } Класс CompileXamlWindow, как обычно, объявлен производным от Window, но в его объявление включено ключевое слово partial. И как обычно, класс содер- жит статический метод Main. Однако конструктор CompileXamlWindow начинается с вызова InitializeComponent. Вроде бы этот метод должен быть частью класса CompileXamlWindow, но его почему-то нигде не видно. Впрочем, вскоре мы позна- комимся с этим методом, а пока достаточно знать, что он выполняет ряд важ- ных функций — например, задает полям Istbox и elips элементы ListBox и Ellipse, созданные на основе XAML, а также устанавливает обработчики событий для элементов Button и ListBox. Конструктор CompileXamlWindow не задает свойство Title или содержимое окна, потому что все это делается в XAML. Впрочем, он должен заполнить список. Оставшаяся часть кода посвящена двум обработчикам событий. Обработчик ButtonOnClick просто вызывает уже знакомое окно сообщения. Обработчик Selection- Changed объекта ListBox изменяет свойство Fill объекта Ellipse. Хотя обработчик со- бытия получает объект ListBox из аргумента Sender, он с таким же успехом может просто обратиться к полю Istbox. Даже если удалить первую команду обработчи- ка, программа все равно будет работать. Если откомпилировать и запустить проект, он будет работать — конечно, это важно, но было бы неплохо также понять, как именно он работает. Загляните в подкаталог obj проекта и откройте в нем подкаталог Release или Debug (в зави- симости от того, как был откомпилирован проект). Вы увидите в нем файл с име- нем CompileXamlWindow.baml. Это расширение означает «Binary XAML» («двоич- ный XAML»). Файл содержит разобранный код XAML, преобразованный в дво- ичную форму. Он включается в исполняемый файл в виде ресурса приложения. В каталоге также находится файл с именем CompileXamlWindow.g.cs. Он со- держит код, сгенерированный на основе XAML-файла (д от слова «generated»). Откройте файл в Блокноте или другом текстовом редакторе. Это другая часть класса CompileXamlWindow, компилируемая наряду с файлом CompileXamlWindow.cs. В начале класса находятся объявления двух полей с именами Istbox и elips. Здесь же расположен метод InitializeComponent, который загружает BAML-файл во вре- мя выполнения и преобразует его в дерево элементов. Ближе к концу файла вы увидите метод, который задает значения полей Istbox и elips и устанавливает об- работчики событий. В самом начале выполнения конструктора CompileXamlWindow свойство Content окна равно null, а все свойства окна (Title, SizeToWindow, ResizeMode и т. д.) имеют значения по умолчанию. После вызова InitializeComponent свойство Content содер- жит объект StackPanel, а другим свойствам заданы значения, указанные в файле XAML. Различные типы связей между кодом XAML и С#, продемонстрированные в программе CompileXamlWindow (совместное использование одного класса, уста-
493 новка обработчиков событий и инициализация полей), возможны только при компиляции XAML с остальным кодом. При загрузке XAML во время выпол- нения прямым (или косвенным) вызовом XamIReader.Load ваши возможности более ограничены. Объекты, созданные в XAML, доступны, но установить об- работчик события или сохранить объект в виде поля будет несколько сложнее. Один из первых вопросов, которые обычно задаются программистами по по- воду XAML, — «А смогу ли я использовать в нем свои классы?» Да, сможете. Чтобы использовать класс из файла С#, откомпилированный в составе проекта, достаточно включить в XAML дополнительное объявление пространства имен. Допустим, пользовательский элемент MyControl находится в файле C# с про- странством имен CLR MyNamespace. Файл C# включается как часть проекта. В файле XAML пространство имен CLR ассоциируется с префиксом (например, stuff). Для этого используется объявление пространства имен следующего вида: xml ns:stuff="clr-namespace:MyNamespace" Текст «clr-namespace» записывается строчными буквами, и за ним должно следовать двоеточие. (В следующей главе рассматривается расширенный синтак- сис, используемый при ссылках на пространства имен во внешних библиотеках динамической компоновки.) Объявление пространства имен должно располагать- ся перед первой ссылкой на MyControl или в виде атрибута элемента MyControl. Перед именем элемента MyControl должен указываться префикс stuff: <stuff:MyControl ... > Какой префикс выбрать? (Полагаю, вы уже отвергли stuff как слишком об- щий термин.) Обычно используются короткие префиксы, но это не обязательно. Если ваш проект содержит исходный код из нескольких пространств имен CLR, для каждого пространства понадобится отдельный префикс, поэтому для предот- вращения путаницы префикс стоит сделать похожим на имя пространства имен CLR. Если все необходимые пользовательские классы находятся в одном про- странстве имен, часто используется префикс sre (от слова «source»). Давайте создадим новый проект с именем UseCustomClass. В него включает- ся ссылка на файл ColorGridBox.cs из проекта SelectColorFromGrid (см. главу 13). Класс ColorGrldBox находится в пространстве имен Petzold.SelectColorFromGrid, по- этому для его использования в файле XAML потребуется объявление следую- щего вида: xml ns:src="clr-namespace:Petzold.SelectColorFromGrid" Далее приводится класс UseCustomClass.xaml с объявлением пространства имен, которое позволяет ссылаться на элемент ColorGrldBox с префиксом src. UseCustomClass.xaml < I - - == === = = ====== = = =: ======: = = = = = = = = = = = = = ^=:=:== = = = = = = = = = UseCustomClass.xaml (с) 2006 by Charles Petzold Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"
494 Глава 19. XAML xml ns:src="clr-namespace:Petzold.Sei ectColorFromGrid" x:Cl ass="Petzold.UseCustomClass.UseCustomClass" Title="Use Custom Class" Si zeToContent="Wi dthAndHei ght" Resi zeMode="CanMi nimi ze"> <StackPanel Orientation="Horizontal"> <Button HorizontalAlignment="Center" Verti cal Ali gnment="Center" Margin="24"> Do-nothing button to test tabbing </Button> <src:ColorGridBox Horizontal Alignment="Center" Vertical Alignment="Center" Margin="24" SelectionChanged="ColorGridBoxOnSelectionChanged" /> <Button HorizontalAlignment="Center" Vertical Alignment="Center" Margin="24"> Do-nothing button to test tabbing </Button> </StackPanel> </Window> Файл программной логики содержит метод Main, вызов InitializeComponent и об- работчик события SelectionChanged для элемента ColorGridBox. UseCustomClass.cs // // UseCustomClass.cs (с) 2006 by Charles Petzold //------------------------------------------------ using Petzol d.Sei ectColorFromGri d; using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: namespace Petzold.UseCustomClass { public partial class UseCustomClass : Window { [STAThread] public static void MainO { Application app = new ApplicationO:
495 XAML___________________________________________________________ app.Run(new UseCustomClassO): } public UseCustomClassO { Initial!zeComponent(): } void ColorGridBoxOnSelectionChanged(object sender, SeiectionChangedEventArgs args) { ColorGridBox clrbox = args.Source as ColorGridBox: Background = (Brush) clrbox.SelectedValue: } } ) Файл UseCustomClass.es должен содержать директиву using для пространст- ва имен Petzold.SelectColorFromGrid, потому что обработчик события ссылается на класс ColorGridBox. Если заменить эту ссылку ссылкой на класс ListBox (от кото- рого является производным ColorGridBox), директиву using можно будет убрать. Более того, можно даже обойтись без обработчика события SelectionChanged, оп- ределяя привязку данных прямо в XAML, но синтаксис выглядит несколько не- обычно, поэтому мы вернемся к нему в главе 23. Ранее я уже упоминал, что для каждого окна приложения (в том числе и для диалоговых окон) обычно создается XAML-файл и соответствующий файл про- граммной логики. Но не стоит ошибочно полагать, будто XAML-файлы не мо- гут использоваться для других элементов, кроме Window. В следующем проекте UseCustomXamICIass пользовательский класс, производный от Button (хотя и весь- ма простой), определяется полностью в XAML. Для определения пользователь- ских классов в XAML используется атрибут x:Class корневого элемента — един- ственное место, в котором может находиться этот атрибут. Следующий файл XAML определяет класс Centered Button, производный от Button. XAML задает свойствам HorizontalAlignment и VerticalAlignment значение Center и назначает кноп- ке небольшие внешние отступы. CenteredButton.xaml <1 __ ================================================= CenteredButton.xaml (с) 2006 by Charles Petzold <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Class="Petzold.UseCustomXamlClass.CenteredButton" HorizontalAlignment=,,Center" Verti ca1 Ali gnment="Center" Margin="12" /> Файл программной логики CenteredButton настолько прост, что в нем нет ни одной директивы using.
496 Глава 19. XAML CenteredButton.es //------------------------------------------------ // CenteredButton.es (с) 2006 by Charles Petzold //------------------------------------------------ namespace Petzold.UseCustomXamlCl ass { public partial class CenteredButton { public CenteredButton!) { InitializeComponent!): } } } Проект также содержит XAML-файл для класса, производного от Window. Кроме обычного атрибута x:Class, этот файл также содержит объявление XML для пространства имен проекта, чтобы на панели StackPanel можно было размес- тить пять экземпляров класса CenteredButton. UseCustomXamlClass.xaml <।- - ===================================================== UseCustomXamlClass.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:src="clr-namespace:Petzold.UseCustomXamlClass" x:Class="Petzold.UseCustomXamlClass.UseCustomXamlClass" Title = "Use Custom XAML Class"> <StackPanel Name="stack"> <src:CenteredButton>Button A</src:CenteredButton> <src:CenteredButton>Button B</src:CenteredButton> <src:CenteredButton>Button C</src:CenteredButton> <src:CenteredButton>Button D</src:CenteredButton> <src:CenteredButton>Button E</src:CenteredButton> </StackPanel> </Window> Обратите внимание: элемент StackPanel содержит атрибут Name со значением stack. Файл программной логики использует это имя для размещения еще пяти кнопок на панели (таким образом, кнопок становится десять). UseCustomXamlClass.cs //--------------------------------------------------- // UseCustomXamlClass.cs (с) 2006 by Charles Petzold //--------------------------------------------------- using System; using System.Windows;
497 XAML using System.Windows.Controls: using System.Windows.Input; using System.Windows.Media: namespace Petzold.UseCustomXamlClass ( public partial class UseCustomXamlCl ass : Window t [STAThread] public static void MainO { Application app = new ApplicationO: app.Run(new UseCustomXamlClassO): } public UseCustomXamlClassO { InitializeComponent(); for (int i = 0: i < 5: 1++) { CenteredButton btn = new CenteredButtonO: btn.Content = "Button No. "+(1+1); stack.Children.Add(btn): } } } ) Кроме XAML-файлов для Window (и, возможно, других элементов), проект также часто содержит XAML-файл для объекта Application. Одно из интересных следствий заключается в том, что в программе отпадает необходимость в явном объявлении метода Main. Давайте посмотрим, как это делается. Следующий проект называется Include- ApplicationDefinition. Он содержит два XAML-файла (для Application и Window) и два соответствующих файла программной логики. XAML-файл объекта Window содержит всего одну кнопку, поэтому он получается совсем коротким. MyWindow.xaml <! -- =========================================== MyWindow.xaml (с) 2006 by Charles Petzold Window xml ns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Class="Petzold.IncludeAppli cati onDefi ni ti on.MyWindow" Title="Include Application Definition" Si zeToContent="Wi dthAndHei ght" Resi zeMode="CanMi nimi ze">
498 Глава 19. Хд^ <Button Horizontal Alignment="Center" VerticalAlignment="Center" Margin="1.5in" Click="ButtonOnCli ck "> Click the Button </Button> </Window> Код XAML использует пространство имен Petzold.IncludeApplicationDefinition и имя класса MyWindow совместно с неполным классом, производным от Window и определенным в MyWindow.cs. Конструктор класса вызывает InitializeComponent и тоже включает обработчик события Click объекта Button. MyWindow.cs //------------------------------------------ // MyWindow.cs (с) 2006 by Charles Petzold //------------------------------------------ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input: namespace Petzold.IncludeApplicati onDefi nition { public partial class MyWindow ; Window { public MyWindowO { InitializeComponentO; } void ButtonOnClick(object sender, RoutedEventArgs args) { Button btn = sender as Button; MessageBox.ShowC'The button Tabled '" + btn.Content + has been clicked."); } } } Второй XAML-файл относится к объекту Application. Он тоже использует про- странство имен Petzold.IncludeApplicationDefinition, но класс называется MyApplication. MyApplication.xaml < I - - ================================================== MyApplication.xaml (с) 2006 by Charles Petzold Application xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml /presentati on"
499 xaml_____________________________________________________________ xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x: Class="Petzold.IncludeApplicationDefinition.MyApplication" StartupUri="MyWindow.xaml" /> Свойство Build Action файла MyApplication.xaml должно быть равно Application- Definition. Будьте внимательны: Visual Studio изменяет значение Build Action в не- которых ситуациях (например, при переименовании файла). Если при исполь- зовании XAML-файла для определения объекта Application компилятор выдает ошибку отсутствия метода Main в проекте, проверьте свойство Build Action XAML- файла объекта Application. Последний атрибут StartupUri ссылается на файл MyWindow.xaml. Конечно, к мо- менту запуска приложения файл MyWindow.xaml уже откомпилирован в файл MyWindow.baml и является ресурсом приложения, но он по-прежнему определяет исходное окно, которое должно отображаться приложением. Атрибут StartupUri занимает место метода Run, обычно вызываемого в Main. Наконец, файл MyApplication.cs не делает вообще ничего. В некоторых прило- жениях он может содержать обработчики событий объекта Application, если они были определены атрибутами файла MyApplication.xaml. MyApplication.es //---------------------------------------------- // MyApplication.es (с) 2006 by Charles Petzold И----------------------------------------------- using System: using System.Windows: namespace Petzold.IncludeApplicationDefinition public partial class MyApplication : Application } } Мы рассмотрели весь проект. Метод Main вроде бы отсутствует, но после ком- пиляции программы он обнаруживается в сгенерированном файле MyApplication.g.cs. Теоретически (хотя и маловероятно для большинства приложений) проект может состоять только из XAML-файлов, без единого файла с программным ко- дом. Такие проекты имеют смысл там, где в XAML определены привязки данных, а также при использовании XAML-анимации. Следующий проект CompileXaml- Only состоит из двух файлов. Первый файл определяет объект Application. XamlOnlyApp.xaml < I _ _ ============================================== XamlOnlyApp.xaml (с) 2006 by Charles Petzold _____ ___________ ____________ > Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" StartupUri="XamlOnlyWindow.xaml" />
500 Глава 19. XAML Как упоминалось ранее, у файла Application свойство Build Action должно быть равно ApplicationDefinition, иначе ничего работать не будет. В свойстве StartupUri указан файл XamlOnlyWindow.xaml, код которого приводится далее. XamlOnlyWindow.xaml <!_ _ ================================================= XamlOnlyWindow.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Title="Compile XAML Only- Si zeToContent="WidthAndHei ght" Resi zeMode="CanMi nimi ze"> <StackPanel> <Button HorizontalAlignment="Center" Margin="24"> Just a Button </Button> <E11 ipse Width="200" Height="100" Margin="24" Stroke="Red" StrokeThickness="10" /> <ListBox Width="100" Height="100" Margin="24"> <Li stBoxItem>Sunday</Li stBoxItem> <Li stBox!tem>Monday</ListBox!tem> <ListBoxItem>Tuesday</ListBoxItem> <Li stBoxItem>Wednesday</Li stBoxItem> <Li stBoxItem>Thursday</ListBoxItem> <Li stBoxItem>Friday</Li stBoxItem> <Li stBoxItem>Saturday</Li stBoxItem> </ListBox> </StackPanel> </Window> Обратите внимание: ни в одном из файлов не определяется имя класса. Про- верка показывает, что Visual Studio генерирует код только для XAML-файла объекта Application и присваивает ему имя класса Application_. Сгенерирован- ный файл содержит метод Main. Для XAML-файла Window кодовый файл не генерируется, но Visual Studio компилирует Window в файл BAML. Для класса Application BAML-файл не нужен, потому что он не определяет дерево элементов на стадии выполнения. Предположим, вы начали работать над приложением, состоящим только из кода XAML, а потом решили добавить в него код С#. Но вам почему-либо не
XAML 501 хочется создавать новый файл C# только для этого кода. К счастью, существует механизм внедрения кода C# в файлы XAML. Способ выглядит довольно урод- ливо, но работает. Проект называется EmbedCodelnXaml, а его первый файл содержит класс App- lication. EmbeddedCodeApp.xaml < ! - - =================================================== EmbeddedCodeApp.xaml (с) 2006 by Charles Petzold <Appl ication xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" StartupUri="EmbeddedCodeWindow.xaml" /> Атрибут StartupUri ссылается на файл EmbeddedCodeWindow.xaml, в котором оп- ределяются объекты Button, Ellipse и ListBox, а также содержится внедренный код С#. EmbeddedCodeWindow.xaml < ! - - ===================================================== EmbeddedCodeWindow.xaml (с) 2006 by Charles Petzold < Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Class="Petzold.Compi1eXamlOnly.EmbeddedCodeWi ndow" Title="Embed Code in XAML" SizeToContent="WidthAndHeight" Resi zeMode="CanMi nimi ze" Loaded="WindowOnLoaded"> <StackPanel> <Button Hori zonta1 Ali gnment="Center" Margin="24" Click="ButtonOnClick"> Click the Button </Button> <E11ipse Name="elips" Width="200" Height="100" Margin="24" Stroke="Red" StrokeThickness="10" /> <ListBox Name="Istbox" Width="150" Height="150" Margin="24"
502 Глава 19. XAML SeiectionChanged="ListBoxOnSelection" /> <x:Code> <![CDATAE void W1ndowOnLoaded(object sender, RoutedEventArgs args) foreach (System.Reflection.Propertyinfo prop in typeof(Brushes).GetProperti es()) Istbox.Items.Add(prop.Name); } void ButtonOnClick(object sender. RoutedEventArgs args) { Button btn = sender as Button; MessageBox.ShowC'The button labeled + btn.Content + has been clicked."); } void ListBoxOnSelection(object sender, SelectionChangedEventArgs args) string strItem = Istbox.Seiectedltem as string; System.Reflection.PropertyInfo prop = typeof(Brushes).GetProperty(strItem); elips.Fill = (Brush)prop.GetValue(null, null); } ]]> </x;Code> </StackPanel> </Window> Внедренный код оформляется с использованием элемента x:Code и секции CDATA элемента x:Code. В спецификации XML секция CDATA определяется как часть XML-файла, предназначенная для хранения «блоков текста, содержащих символы, которые могут распознаваться как разметка». Несомненно, это отно- сится к символам C# и других языков программирования. Секция CDATA всегда начинается со строки <![CDATA[ и всегда завершается строкой ]]>. Внутри секции CDATA последовательность символов ]]> не может присутствовать ни при каких условиях. Во время компиляции проекта код C# переносится в файл EmbeddedCode- Window.g.cs. Внедренный код не может содержать определения полей. Возмож- но, в нем придется использовать полную запись пространств имен, если сгене- рированный код не включает директивы using для этих пространств. Учтите, что внедренный код в файле EmbeddedCodeWindow.xaml должен использовать полно- стью уточненные имена классов в пространстве имен System.Reflection. Внедрение кода C# в XAML-файлы иногда бывает удобно, но синтаксис слиш- ком уродлив и неуклюж, чтобы его можно было рекомендовать как универсаль- ное решение. Постарайтесь никогда не использовать внедрение кода в XAML- файлы, а еще лучше — забыть, что вам о нем известно.
XAML______________________________________:________________________________503 Помните программу DesignAButton из главы 5? Программа задает объект StackPanel свойству Content объекта Button, а затем украшает StackPanel двумя объектами Polyline, объектом Label и Image со значком BOOK06.ICO, который я взял из библиотеки изображений Visual Studio. Давайте попробуем воспроизвести эту программу в виде проекта, содержащего только XAML, а вернее, только XAML и файл значка. Свойству Build Action файла значка должно быть задано значение Resource, а у приведенного далее файла DesignXamlButtonApp.xaml оно должно быть равно Application Definition. DesignXamlButtonApp.xaml <! - - ====================================================== DesignXamlButtonApp.xaml (с) 2006 by Charles Petzold <Appl i cati on xmlns="http://schemas.mlcrosoft.com/wi nfx/2006/xaml/presentation" StartupUri="DesignXamlButtonWindow.xaml" /> Завершающей частью проекта является XAML-файл для объекта Window. У него свойство Build Action должно быть равно Раде. DesignXamlButtonWindow.xaml <! - - ========================================================= DesignXamlButtonWindow.xaml (с) 2006 by Charles Petzold «Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Title="Design XAML Button" Si zeToContent="Wi dthAndHei ght" Resi zeMode="CanMi nimi ze"> <Button HorizontalAlignment="Center" Vertical Alignment="Center" Margin="24"> <StackPanel> <Polyline Stroke="Black" Points="0 10,10 0.20 10,30 0,40 10,50 0, 60 10,70 0,80 10,90 0,100 10" /> <Image Margin="0,10,0,0" Source="B00K06.ICO" Stretch="None" /> <Label Horizontal Alignment="Center"> _Read Books! </Label> <Polyline Stroke="Black"
504 Глава 19. XAML Points="0 0,10 10,20 0,30 10,40 0,50 10, 60 0,70 10,80 0,90 10,100 0" /> </StackPanel> </Button> </Window> Окно содержит только объект Button, но объект Button содержит StackPanel a StackPanel содержит четыре остальных элемента. Два элемента Polyline задают серию из 11 точек, определяемых в виде пар координат. Обратите внимание на разделение точек запятыми. Также можно использовать пробелы для разделе- ния точек, а запятые — для разделения координат каждой точки, или использо- вать пробелы или запятые в обеих целях. Элемент XAML Image реализован весьма элегантно. Вместо того чтобы опре- делять объект Uri, а затем задавать объект Bitmapimage свойству Source объекта Image (все эти действия необходимы в коде С#, который приведен в исходном файле DesignAButton.cs), вы просто задаете свойству Source имя файла значка. При желании можно использовать полный идентификатор URI для ресурса значка (pack://application:zz/BOOK06.ICO), но, на мой взгляд, запись с одним именем файла выглядит более компактно. XAML включает немало подобных вспомогательных средств; мы рассмотрим их, начиная со следующей главы. Но сначала нам понадобится программа для интерактивных экспериментов с XAML и исследования его синтаксиса.
Свойства и атрибуты Метод XamIReader.Load, упоминавшийся в предыдущей главе, подсказывает идею удобной вспомогательной программы. Допустим, имеется объект TextBox, а для события TextChanged установлен обработчик события. При вводе кода XAML в TextBox обработчик события TextChanged может попытаться передать XAML методу XamIReader.Load и отобразить полученный объект. Вызов XamIReader.Load необходимо заключить в блок try, потому что в боль- шинстве случаев код XAML остается недействительным в процессе ввода, однако такая программа могла бы обеспечить немедленную обратную связь при экспе- риментах с XAML. Подобный инструмент весьма полезен для изучения XAML, да и написать его было бы интересно. Конечно, XAML Cruncher — не первая программа в этой категории, да и не последняя. XAML Cruncher строится на базе Клона Блокнота. Как будет вскоре показано, в XAML Cruncher объект TextBox, заполняющий клиентскую область, заменяется объектом Grid. В одной ячейке Grid находится объект TextBox, в дру- гой — элемент Frame, а между ними находится разделитель GridSplitter. Если код XAML, введенный в текстовом поле, успешно преобразуется в объект методом XamIReader.Load, этот объект назначается содержимым Frame. Проект XamlCruncher содержит все файлы проекта Notepadclone, кроме Note- padCloneAssemblyInfo.cs. Этот файл заменяется следующим файлом. XamlCruncherAssemblyInfo.cs //--------------------------------------------------------- // XamlCruncherAssemblyInfo.cs (с) 2006 by Charles Petzold //--------------------------------------------------------- using System.Reflection; [assembly; AssemblyTitleC'XAML Cruncher")] [assembly; Assemblyproduct("XamlCruncher")] [assembly; AssemblyDescriptionC'Programming Tool Using XamIReader.Load")] [assembly; Assemblycompany("www.charlespetzold.com")] [assembly; AssemblyCopyright("\xOOA9 2006 by Charles Petzold")] [assembly; AssemblyVersion(" 1.0.*")] [assembly; AssemblyFi 1 eVersion(" 1.0.0.0")]
506 Глава 20. Свойства и атрибуты Напомню, что класс NotepadCloneSettings содержит несколько параметров, со. храняемых в качестве пользовательской конфигурации. Класс XamlCruncherSettings, производный от NotepadCloneSettings, добавляет в него всего три параметра. Первый — Orientation — определяет ориентацию TextBox и Frame. Одна из команд меню XAML Cruncher позволяет разместить один объект над другим или рядом друг с другом по горизонтали. Кроме того программа переопределяет стандартную для TextBox обработку клавиши Tab и за- меняет ее нажатие пробелами. Второй пользовательский параметр определяет количество пробелов, вставляемых при нажатии клавиши Tab. Третий параметр содержит строку с простым кодом XAML, который выво- дится в поле TextBox при запуске программы, а также при выполнении команды File ► New. Другая команда меню позволяет заменить его текущим содержимым TextBox. XamlCruncherSettings.cs //------------------------------------------------------ // XamlCruncherSettings.cs (c) 2006 by Charles Petzold // using System: using System.Windows: using System.Windows.Controls; using System.Windows.Media; namespace Petzold.XamlCruncher { public class XamlCruncherSettings : Petzold.NotepadClone.NotepadCloneSetti ngs { // Пользовательские настройки по умолчанию public Dock Orientation = Dock.Left: public int TabSpaces = 4; public string StartupDocument = "<Button xmlns=\"http://schemas.microsoft.com/winfx" + ,72006/xaml/presentation\"\r\n" + " xml ns:x=\"http://schemas.microsoft.com/winfx" + ,72006/xaml\">\r\n" + " Hello. XAML!\r\n" + "</Button>\r\n": // Конструктор для инициализации настроек по умолчанию public Xam1CruncherSettings() FontFamily = "Lucida Console": FontSize =10/0.75; } }
Свойства и атрибуты 507 Конструктор XamICruncherSettings выбирает по умолчанию шрифт Lucida Con- sole размером 10 пунктов. Конечно, после запуска XAML Cruncher его можно заменить любым другим шрифтом. Класс XamICruncher объявлен производным от класса Notepadclone. Этот класс отвечает за создание объекта Grid, который становится новым содержимым Window, а также за создание меню верхнего уровня Xaml и шести команд этого подменю. XamlCruncher.cs // XamlCruncher.cs (с) 2006 by Charles Petzold И---------------------------------------------- using System; using System.10; // для StringReader using System.Text; // для StringBuiIder using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; // для StatusBarltem using System.Windows.Input; using System.Windows.Markup; // для XamIReader.Load using System.Windows.Media; using System.Windows.Thread!ng: // для DispatcherllnhandledExceptionEventArgs using System.Xml; // для XmlTextReader namespace Petzold.XamlCruncher { class XamICruncher ; Petzold.Notepadclone.Notepadclone Frame frameParent; // Для отображения объекта, созданного из XAML Window win; // Объект Window, созданный из XAML StatusBarltem statusParse; // Сообщение об ошибке или 0К int tabspaces = 4; // При нажатии клавиши Tab // Загружаемые настройки XamICruncherSettings settingsXaml; // Поддержка меню XamlOri entati onMenuItern i temOri entati on; bool isSuspendParsing = false; [STAThread] public new static void MainO { Application app = new ApplicationO: app.ShutdownMode = ShutdownMode.OnMainWindowClose; app.Run(new XamlCrunched)); } // Открытое свойство для приостановки разбора public bool IsSuspendParsing
508 Глава 20. Свойства и атрибуты set { 1sSuspendParsing = value; } get { return isSuspendParsing; } } // Конструктор public XamlCruncher() // Новый фильтр для диалоговых окон // открытия и сохранения файлов strFilter = "XAML F11es(*.xaml)|*.xaml|Al 1 F11es(*.*)|*.*"; // Найти объект DockPanel и удалить из него TextBox DockPanel dock = txtbox.Parent as DockPanel; dock.Children.Remove(txtbox); // Создать панель Grid с тремя строками и столбцами // нулевого размера Grid grid = new GridO; dock.Chi 1dren.Add(grid); for (int 1=0; i < 3; 1++) RowDefinition rowdef = new RowDefinitionO; rowdef.Height = new GridLength(O); gri d.RowDef1ni ti ons.Add(rowdef); ColumnDefinition coldef = new ColumnDefinition(); coldef.Width = new GridLength(O); gri d.ColumnDef1ni ti ons.Add(col def); } // Инициализация первой строки и столбца 100* grid.RowDefinitionsEO].Height = new GridLengthdOO. GridUnitType.Star); grid.ColumnDefinitionsEO].Width = new GridLengthdOO. GridUnitType.Star); // Добавление к Grid двух элементов GridSplitter GridSplitter split = new GridSplitterO; split.HorizontalAlignment = HorizontalAlignment.Stretch; split.VerticalAlignment = VerticalAlignment.Center; split.Height = 6; grid.Chi 1dren.Add(split); Grid.SetRow(split. 1); Grid.SetColumn(split, 0); Grid.SetColumnSpan(split. 3); split = new GridSplitterO; split.HorizontalAlignment = HorizontalAlignment.Center;
509 Свойства^ атрибуты____________________________________________________ split.VerticalAlignment = VerticalAlignment.Stretch; split.Height = 6; grid.ChiIdren.Add(split); Grid.SetRow(split. 0); Grid.SetColumn(split, 1); Grid.SetRowSpan(split, 3); // Создание объекта Frame для отображения объекта XAML frameParent = new FrameO; frameParent. Navi gati onlllVi si bi 1i ty = NavigationUIViSibility.Hidden; gri d.Chi 1dren.Add(frameParent); // Включение в Grid объекта TextBox txtbox.TextChanged += TextBoxOnTextChanged; grid.Chi 1dren.Add(txtbox): // Упаковка загруженных настроек в XamlCruncherSettings settingsXaml = (XamlCruncherSettings)settings; // Включение команды "Xaml" в меню верхнего уровня Menuitem ItemXaml = new MenuItemO; itemXaml.Header = "_Xaml"; menu.Items.Insert(menu.Items.Count - 1, itemXaml); // Создание объекта XamlOrientationMenuItem // и включение его в меню itemOrientation = new XamlOrientationMenuItem(grid. txtbox, frameParent); itemOrientation.Orientation = settingsXaml.Orientation; itemXaml.Items.Add(itemOrientation); // Команда меню для задания величины табуляции Menuitem itemTabs = new MenuItemO; itemTabs.Header = "_Tab Spaces..."; itemTabs.Click += TabSpacesOnClick; itemXaml.Items.Add(itemTabs); // Команда меню для приостановки разбора Menuitem ItemNoParse = new MenuItemO; ItemNoParse.Header = "-Suspend Parsing"; ItemNoParse.IsCheckable = true; 1temNoParse.SetBi ndi ng(MenuItem.IsCheckedProperty, "IsSuspendParsing"); ItemNoParse.DataContext = this; itemXaml.Items.Add(ItemNoParse); // Команда для возобновления разбора InputGestureCollection collGest = new InputGestureCollection();
510 Глава 20. Свойства и атрибуты collGest.Add(new KeyGesture(Key.F6)); RoutedUICommand commReparse = new RoutedUICommand("_Reparse". "Reparse". GetTypeO. collGest); // Команда меню для повторного разбора Menuitem itemReparse = new MenuItemO; itemReparse.Command = commReparse; itemXaml.Items.Add(itemReparse); // Привязка команды повторного разбора CommandBi ndi ngs.Add(new CommandBindi ng(commReparse. ReparseOnExecuted)); // Команда для отображения окна InputGestureCollection collGest = new InputGestureCollectionO; collGest.Add(new KeyGesture(Key.F7)); RoutedUICommand commShowWin = new RoutedUICommandC'Show -Window", "ShowWindow". GetTypeO. collGest); // Команда меню для отображения окна Menuitem itemShowWin = new MenuItemO; itemShowWin.Command = commShowWin; itemXaml.Iterns.Add(itemShowWin); // Привязка команды для отображения окна CommandBi ndi ngs.Add(new CommandBi ndi ng(commShowWi n, ShowWi ndowOnExecuted, ShowWi ndowCanExecute)); // Команда меню для сохранения текущего содержимого // в качестве нового исходного документа Menuitem itemTemplate = new MenuItemO; itemTemplate.Header = "Save as Startup -Document"; itemTemplate.Click += NewStartupOnClick; itemXaml.Items,Add(itemTemplate); // Включение команды Help в меню Help Menuitem itemXamlHelp = new MenuItemO; itemXamlHelp.Header = "_Help..."; itemXaml Help.Click += HelpOnClick; Menuitem itemHelp = (Menultem)menu.Items[menu.Items.Count - 1]; itemHelp.Items.Insert(O, itemXamlHelp); // Новый компонент StatusBar statusParse = new StatusBarltemO; status.Items.Insert(O, statusParse); status.Visibility = Visibility.Visible;
гяойства и атрибуты 511 // Установка обработчика для необработанного исключения // Закомментируйте этот код на время экспериментов // с новыми функциями или внесение изменений в программу! Di spatcher. Unha nd 1 edExcepti on += Di spatcherOnllnhandl edExcept 1 on: } // Переопределенный обработчик NewOnExecute // помещает новый исходный документ в TextBox protected override void NewOnExecute(object sender, ExecutedRoutedEventArgs args) base.NewOnExecute(sender, args); string str = ((XamlCruncherSettings)settings).StartupDocument; // Позаботиться о том, чтобы следующая операция замены // не заменила слишком много str = str.Replace("\r\n", "\n"); // Замена символов новой строки (CR) комбинацией CR+LF str = str.Replace("\n", "\r\n"); txtbox.Text = str; isFileDirty = false; } // Переопределение LoadSettings загружает XamlCrunchersettings protected override object LoadSettingsO { return XamlCruncherSetti ngs.Load(typeof(XamlCruncherSetti ngs), strAppData); } // Переопределение OnClosed сохраняет значение Orientation // из свойства команды меню protected override void OnClosed(EventArgs args) { settingsXaml.Orientation = itemOrientation.Orientation; base.OnClosed(args); } // Переопределение SaveSettings сохраняет // объект XamlCruncherSettings protected override void SaveSettings() { ((XamlCruncherSettings)settings).Save(strAppData): } // Обработчик команды меню Tab Spaces void TabSpacesOnClick(object sender, RoutedEventArgs args) { XamlTabSpacesDialog dig = new XamlTabSpacesDialogO;
512 Глава 20. Свойства и атрибуты dig.Owner = this: dig.TabSpaces = settingsXaml.TabSpaces: If ((bool)dlg.ShowDIalog().GetValueOrDefaultO) { settingsXaml.TabSpaces = dig.TabSpaces: } } // Обработчик команды меню Reparse void ReparseOnExecuted(object sender. ExecutedRoutedEventArgs args) { ParseO: } // Обработчики команды меню Show Window void ShowWindowCanExecute(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = (win != null): } void ShowW1ndow0nExecuted(object sender. ExecutedRoutedEventArgs args) { if (win != null) win.Show(): } // Обработчик команды Save as New Startup Document void NewStartupOnClick(object sender, RoutedEventArgs args) { ((XamlCruncherSettings)settings).StartupDocument = txtbox.Text; } // Команда меню Help void HelpOnClick(object sender, RoutedEventArgs args) { Uri uri = new Uri("pack://application:,../XamlCruncherHelp.xaml"); Stream stream = Application.GetResourceStream(uri).Stream: Window win = new WindowO; win.Title = "XAML Cruncher Help"; win.Content = XamlReader.Load(stream): win.ShowO: } // Метод OnPreviewKeyDown заменяет табуляцию пробелами protected override void OnPreviewKeyDown(KeyEventArgs args)
свойства и атрибуты 513 ba se.OnPrev1ewKeyDown(args); if (args.Source == txtbox && args.Key == Key.Tab) { string strinsert = new stringU tabspaces); int iChar = txtbox.Selectionstart; int iLine = txtbox.GetLinelndexFromCharacterlndex(iChar); if (iLine != -1) { int iCol = iChar - txtbox.GetCharacterIndexFromL1neIndex(ILine); strinsert = new stringU settingsXaml.TabSpaces - iCol Я settingsXaml.TabSpaces); txtbox.SeiectedText = strinsert; txtbox.Caretindex = txtbox.Selectionstart + txtbox.Seiecti onLength; args.Handled = true; } } // Метод TextBoxOnTextChanged пытается разобрать код XAML void TextBoxOnTextChanged(object sender, TextChangedEventArgs args) { if (IsSuspendParsing) txtbox.Foreground = SystemColors.WindowTextBrush; else ParseO; } // Обобщенный метод Parse также вызывается для команды Reparse void ParseO { StringReader strreader = new StringReader(txtbox.Text); XmlTextReader xml reader = new XmlTextReader(strreader); try { object obj = XamIReader.Load(xmlreader); txtbox.Foreground = SystemColors.WindowTextBrush; if (obj is Window) { win = obj as Window: statusParse.Content = "Press F7 to display Window": } else { win = null; frameParent.Content = obj;
514 Глава 20. Свойства и атрибуты statusParse.Content = "OK": } } catch (Exception exc) { txtbox.Foreground = Brushes.Red: statusParse.Content = exc.Message: } } // Обработчик UnhandledException потребуется в том случае, // если объект XAML инициирует исключение void DispatcherOnUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs args) { statusParse.Content = "Unhandled Exception: " + args.Exception.Message; args.Handled = true: } } } Метод Parse, расположенный в конце класса, отвечает за разбор кода XAML. Если XamIReader.Load инициирует исключение, обработчик окрашивает содержи- мое TextBox в красный цвет и выводит сообщение об исключении в строке со- стояния. В противном случае объект задается свойству Content элемента Frame. Для объекта типа Window предусмотрена специальная обработка. Объект сохра- няется в поле и ожидает нажатия клавиши F7 для запуска в отдельном окне. Иногда дерево элементов, созданное на базе кода XAML, по какой-то причи- не инициирует исключение. Так как объект, созданный по коду XAML, являет- ся частью приложения, исключение может привести к аварийному завершению XAML Cruncher, хотя приложение ни в чем не виновато. По этой причине про- грамма устанавливает обработчик события UnhandledException и обрабатывает со- бытие с выводом сообщения в строке состояния. В общем случае программы не должны устанавливать этот обработчик события, кроме тех случаев, когда воз- никающие исключения могут быть не связаны с ошибкой в коде программы (как XAML Cruncher). Команда меню, позволяющая изменить размер табуляции в пробелах, выво- дит диалоговое окно. Макет окна определяется XAML-файлом. XamlTabSpacesDialog.xaml <।_ _ ====================================================== XamlTabSpacesDialog.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Cl ass="Petzold.XamlCruncher.XamlTabSpacesDi a1og"
Свойства и атрибуты 515 ———' Title="Tab Spaces" Wi ndowSty1e="ToolWindow" Si zeToContent="Wi dthAndHei ght" Resi zeMode="NoRes1ze" Wi ndowStartupLocati on="CenterOwner"> <StackPanel> <StackPanel Orientation="Horizontal"> <Label Margin="12,12,6,12"> _Tab spaces (1-10): </Label> <TextBox Name="txtbox" TextChanged-'TextBoxOnTextChanged" Margin="6,12,12,12"/> </StackPanel> <StackPanel Orientation="Horizontal"> <Button Name="btnOk" Click="OkOnClick" IsDefault="True" IsEnabled="False" Margin="12"> OK </Button> <Button IsCancel="True" Margin="12"> Cancel </Button> </StackPanel> </StackPanel> </Window> Если вы хорошо усвоили содержимое предыдущей главы, с этим XAML-фай- лом проблем быть не должно. Файл программной логики определяет открытое свойство TabSpaces и два обработчика событий. XamlTabSpacesDialog.cs И И XamlTabSpacesDialog.cs (с) 2006 by Charles Petzold И using System; using System.Windows; using System.Windows.Controls; namespace Petzold.XamlCruncher { public partial class XamlTabSpacesDialog { public XamlTabSpacesDialogO {
516 Глава 20. Свойства и атрибуты Initial izeComponentO: txtbox.Focus(): } public Int TabSpaces { set { txtbox.Text = value.ToString(); } get { return Int32.Parse(txtbox.Text); } } void TextBoxOnTextChanged(object sender, TextChangedEventArgs args) { int result: btnOk.IsEnabled = (Int32.TryParse(txtbox.Text. out result) && result > 0 && result < 11): } void OkOnClick(object sender. RoutedEventArgs args) { DialogResult = true: } } } Класс определяет свойство с именем TabSpaces, которое напрямую обращает- ся к свойству Text объекта TextBox. Обратите внимание: метод доступа get вызы- вает статический метод Parse структуры Int32 с полной уверенностью, что это не приведет к выдаче исключения. Такая уверенность возникает в результате дей- ствий обработчика TextChanged, который снимает блокировку с кнопки ОК лишь после того, как статический метод TryParse вернет true, а введенное значение бу- дет лежать в диапазоне от 1 до 10. Класс XamICruncher вызывает это диалоговое окно, когда пользователь выби- рает в меню команду Tab Spaces. Полное содержимое обработчика Click для этой команды меню выглядит так: XamlTabSpacesDialog dig = new XamlTabSpacesDialog(): dig.Owner = this: dig.TabSpaces = settingsXaml.TabSpaces; if ((bool)dlg.ShowDialog().GetValueOrDefault()) { settingsXaml.TabSpaces = dig.TabSpaces: } Задание свойства Owner гарантирует, что заданное в XAML-файле значение WindowStartupLocation будет нормально работать. Если пользователь закроет диа- логовое окно кнопкой Cancel, при обращении к свойству TabSpaces могут возник- нуть проблемы. Если пользователь щелкнет на кнопке ОК, свойство TabSpaces заведомо не инициирует исключение. Команда меню для изменения ориентации объектов TextBox и Frame обладает собственным классом, обозначающим четыре возможные ориентации небольши- ми рисунками. Класс также должен заново разместить элементы на панели Grid при выборе новой ориентации пользователем.
Свойства и атрибуты 517 XamlOrientationMenuItem.cs // XamlOrientat1onMenuItem.cs (с) 2006 by Charles Petzold И using System; using System.Globalization: using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace Petzold.XamICruncher class XamlOrientationMenuItem ; Menuitem { Menu Item itemChecked; Grid grid; TextBox txtbox; Frame frame; // Orientation - открытое свойство типа Dock public Dock Orientation { set { foreach (Menuitem item in Items) if (item.IsChecked = (value == (Dock)item.Tag)) itemChecked = item; } get { return (Dock)itemChecked.Tag: } } // Конструктор получает три аргумента public XamlOrientationMenuItem(Grid grid. TextBox txtbox, Frame frame) { this.grid = grid; this.txtbox = txtbox; this.frame = frame; Header = "_Orientation"; for (int i = 0; i < 4; 1++) Items.Add(CreateItem((Dock)i)); (itemChecked = (Menuitem) Items[OJ).IsChecked = true; } // Создание команд меню на основании значения Dock Menuitem CreateItem(Dock dock)
518 Глава 20. Свойства и атрибуты { Menuitem item = new MenuItemO; item.Tag = dock; item.Click += ItemOnClick; item.Checked += ItemOnCheck; // Две строки, присутствующие в команде меню FormattedText formtxtl = CreateFormattedTextC'Edit"); FormattedText formtxt2 = CreateFormattedText("Display"); double widthMax = Math.Max(formtxtl.Width, formtxt2.Width); // Создание объектов DrawingVisual и DrawingContext DrawingVisual vis = new DrawingVisual0; DrawingContext de = vis.RenderOpenО: // Вывод текста в рамке switch (dock) { case Dock.Left; // Редактор слева, вывод справа BoxText(dc, formtxtl, formtxtl.Width, new Point(0, 0)); BoxText(dc, formtxt2, formtxt2.Width. new Point(formtxtl.Width +4. 0)); break; case Dock.Top: // Редактор сверху, вывод внизу BoxText(dc. formtxtl, widthMax, new Point(0, 0)); BoxText(dc, formtxt2, widthMax, new Point(0, formtxtl.Height + 4)); break; case Dock.Right: // Редактор справа, вывод слева BoxText(dc, formtxt2, formtxt2.Width, new PointCO. 0)); BoxText(dc. formtxtl, formtxtl.Width, new Point(formtxt2.Width +4, 0)); break: case Dock.Bottom: // Редактор внизу, вывод сверху BoxText(dc, formtxt2, widthMax, new PointCO, 0)): BoxText(dc, formtxtl, widthMax, new Point(0. formtxt2.Height + 4)): break; } dc.Close(); // Создание объекта Image на базе свойства Drawing // объекта DrawingVisual Drawingimage drawimg = new Drawinglmage(vis.Drawing); Image img = new ImageO: img.Source = drawimg; // Свойству Header команды меню задается объект Image item.Header = img;
Свойства и атрибуты 519 return item; } // Обработка аргументов FormattedText FormattedText CreateFormattedText(string str) { return new FormattedText(str, Cultureinfo.Currentculture. FlowDirecti on.LeftToRight, new Typeface(SystemFonts.MenuFontFamily, SystemFonts.MenuFontSty1e, SystemFonts.MenuFontWeight. FontStretches.Normal). SystemFonts.MenuFontSi ze. SystemColors.MenuTextBrush); } // Вывод текста, заключенного в прямоугольник void BoxText(DrawingContext de. FormattedText formtxt. double width. Point pt) { Pen pen = new Pen(SystemColors.MenuTextBrush. 1); dc.DrawRectangle(null. pen. new Rect(pt.X. pt.Y. width + 4. formtxt.Height + 4)); double X = pt.X + (width - formtxt.Width) / 2; de.DrawText(formtxt. new Point(X + 2. pt.Y + 2)); } // Установка и снятие пометки команд меню void ItemOnClick(object sender. RoutedEventArgs args) { itemChecked.IsChecked = false; itemChecked = args.Source as Menuitem; itemChecked.IsChecked = true; } // Изменение ориентации в зависимости от помеченной команды void ItemOnCheck(object sender. RoutedEventArgs args) { Menuitem itemChecked = args.Source as Menuitem: // Инициализация размеров строк и столбцов нулями for (Int i = 1: i < 3: i++) { grid.RowDefinitions[i].Height = new GridLength(O); grid.ColumnDefinitions[i],Width = new GridLength(O): } // Инициализация ячеек TextBox и Frame нулями Grid.SetRow(txtbox. 0); Grid.SetColumn(txtbox. 0);
520 Глава 20. Свойства и атрибуты Gri d.SetRow( frame. 0); Grid.SetColumn(frame, 0); // Настройка строк и столбцов в зависимости от ориентации switch ((Dock)itemChecked.Tag) { case Dock.Left: // Редактор слева, вывод справа grid.ColumnDefinitions[l].Width = GridLength.Auto; grid.ColumnDefinitions[2].Width = new GridLengthdOO, GridUnitType.Star); Grid.SetColumn(frame, 2); break; case Dock.Top: // Редактор наверху, вывод внизу grid.RowDefinitions[l].Height = GridLength.Auto: grid.RowDefinitions[2].Height = new GridLengthdOO, GridUnitType.Star); Grid.SetRow(frame, 2); break; case Dock.Right: // Редактор справа, вывод слева grid.ColumnDefinitionsEl].Width = GridLength.Auto; grid.ColumnDefinitions[2].Width = new GridLengthdOO, GridUnitType.Star); Grid.SetColumn(txtbox, 2): break: case Dock.Bottom: // Редактор внизу, вывод наверху grid. RowDefi ni ti onsLU. Height = GridLength.Auto: grid.RowDefinitions[2],Height = new GridLengthdOO. GridUnitType.Star); Grid.SetRow(txtbox, 2); break: } } } } Конструктор класса XamICruncher также добавляет в меню верхнего уровня Help подкоманду Help. Команда отображает окно с коротким описанием програм- мы и новых команд меню. Когда-то справочная система могла храниться в фор- мате RTF или причудливом, но популярном формате HTML. Однако мы проде- монстрируем свою приверженность новым технологиям и оформим справку в виде объекта FlowDocument — объекты этого типа создаются полем RichTextBox. В первых версиях XAML Cruncher я кодировал этот файл вручную. В основном его содержимое понятно без объяснений, потому что его элементы определяют знакомые атрибуты шрифтового оформления — Paragraph, Bold, Italic и т. д. XamlCruncherHelp.xaml XamlCruncherHelp.xaml (с) 2006 by Charles Petzold
Свойства и атрибуты 521 FlowDocument xmlns="http://schemas.mlcrosoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" TextAlignment="Left"> Paragraph TextAlignment="Center" FontSize="32" FontStyle="Italic" LineHeight="24"> XAML Cruncher </Paragraph> Paragraph TextAlignment="Center"> &#x00A9; 2006 by Charles Petzold </Paragraph> Paragraph FontSize="16pt" FontWeight="Bold" LineHeight="16"> Introduction </Paragraph> <Paragraph> XAML Cruncher is a sample program from Charles Petzold's book <Italic> Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation </Italic> published by Microsoft Press in 2006. XAML Cruncher provides a convenient way to learn about and experiment with XAML, the Extensible Application Markup Language. </Paragraph> <Paragraph> XAML Cruncher consists of an Edit section (in which you enter and edit a XAML document) and a Display section that shows the object created from the XAML. If the XAML document has errors, the text is displayed in red and the status bar indicates the problem. </Paragraph> <Paragraph> Most of the interface and functionality of the edit section of XAML Cruncher is based on Windows Notepad. The <Bold>Xaml</Bold> menu provides additional features. </Paragraph> Paragraph FontSize="16pt" FontWeight="Bold" LineHeight="16"> Xaml Menu </Paragraph> <Paragraph> The <Bold>Orientation</Bold> menu item lets you choose whether you want the Edit and Display sections of XAML Cruncher arranged horizontally or vertically. </Paragraph>
522 Глава 20. Свойства и атрибуты <Paragraph> The <Bold>Tab Spaces</Bold> menu item displays a dialog box that lets you choose the number of spaces you want inserted when you press the Tab key. Changing this item does not change any indentation already in the current document. </Paragraph> <Paragraph> There are times when your XAML document will be so complex that it takes a little while to convert it into an object. You may want to <Bold>Suspend Parsing</Bold> by checking this item on the <Bold>Xaml</Bold> menu. </Paragraph> <Paragraph> If you've suspended parsing, or if you want to reparse the XAML file, select <Bold>Reparse</Bold> from the menu or press F6. </Paragraph> <Paragraph> If the root element of your XAML is <Italic>Window</Italic>. XAML Cruncher will not be able to display the <Italic>Window</Italic> object in its own window. Select the <Bold>Show Window</Bold> menu item or press F7 to view the window. </Paragraph> <Paragraph> When you start up XAML Cruncher (and whenever you select <Bold>New</Bold> from the <Bold>Fi1e</Bold> menu), the Edit window displays a simple startup document. If you want to use the current document as the startup document, select the <Bold>Save as Startup Document</Bold> item. </Paragraph> </FlowDocument> Файл должен быть обозначен как ресурс (Resource) в проекте XamlCruncher. Visual Studio пытается назначить ему тип Раде. Обработчик события HelpOnClick сначала получает объект URI для ресурса и создает для него объект Stream: Uri uri = new Uri("pack://application:.../XamlCruncherHelp.xaml"); Stream stream = Application.GetResourceStream(uri).Stream; Затем метод создает объект Window, инициализирует его свойство Title и зада- ет свойству Content объект FlowDocument, возвращаемый методом XamIReader.Load при получении объекта Stream, ссылающегося на ресурс: Window win = new WindowO; win.Title = "XAML Cruncher Help"; win.Content = XamlReader.Load(stream); win.ShowO:
Свойства и атрибуты 523 Также существует пара альтернативных решений. Например, вы можете со- здать элемент Frame и задать его свойству Source объект Uri: Frame frame = new FrameO; frame.Source = new Uri("pack://application:.,/XamlCruncherHelp.xaml”); Далее создается объект Window, содержимому которого задается объект Frame: Window win = new WindowO: win.Title = "XAML Cruncher Help": win.Content = frame: win.Show(): Еще один вариант: создайте XAML-файл с определением окна справки (до- пустим, XamlHelpDialog.xaml): <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Title="XAML Cruncher Help" x:Class="Petzold.XamlCruncher.XamlHelpDi alog"> <Frame Source="XamlCruncherHelp.xaml" /> </Window> Обратите внимание на упрощенный синтаксис задания свойства Source эле- мента Frame. Метод HelpOnClick сводится к следующим трем командам: XamlHelpDialog win = new XamlHelpDialog(); win. Initial izeComponentO: win.Show(): После создания объекта XamIHelpDialog, определяемого в XAML, метод вызыва- ет сначала InitializeComponent (обычно это делается в файле программной логики), а затем Show. Интересная особенность: аспект такого решения заключается в том, что свойство Build Action файла XamlCruncherHelp.xaml с определением FlowDocument может быть равно как Resource, так и Раде. В последнем случае XAML-файл со- храняется в файле .EXE в виде откомпилированного BAML-файла. Независимо от того, как будет отображаться справочный файл, программа XAML Cruncher полностью готова к использованию. В этой и нескольких сле- дующих главах будут демонстрироваться автономные XAML-файлы, которые можно создавать и запускать в ХАМ Cruncher или другой похожей программе. Начнем с простого XAML-файла, создаваемого XAML Cruncher по умолчанию: <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml> Hello. XAML! </Button> Статический метод XamIReader.Load разбирает этот текст и создает объект типа Button. Для краткости я буду называть «парсером» метод XamIReader.Load, который разбирает код XAML и создает по нему один или несколько объектов. Первый вопрос, который обычно возникает у программистов: откуда парсер знает, какой класс должен использоваться для создания конкретного объекта
524 Глава 20. Свойства и атрибуты Button? В конце концов, в .NET существуют три разных класса Button (два дру- гих — в Windows Forms и ASP.NET). Хотя XAML-файл содержит два простран- ства имен XML, он не содержит упоминаний пространств имен CLR (например, System.Windows.Controls). Кроме того, XAML не содержит ссылок на сборку Presen- tationFramework.dll, в которой находится класс System.Windows.Controls.Button. По- чему парсер не требует полного уточнения имени класса или какого-нибудь ана- лога директивы using? Скорее всего, приложению WPF не потребуются ссылки на сборки Windows Forms и сборки ASP.NET, в которых находятся другие классы Button, но в нем определенно должна присутствовать ссылка на сборку PresentationFramework.dll, в которой находится класс XamIReader. Но даже если приложение WPF будет со- держать ссылки на System.Windows.Forms.dll или System.Web.dll и эти сборки будут загружены приложением, парсер все равно будет знать, какой класс Button сле- дует использовать. Ответ на эту загадку кроется в сборке PresentationFramework, содержащей ряд пользовательских атрибутов. (Чтобы получить их значения, приложение вызы- вает метод GetCustomAttributes для объекта Assembly.) Некоторые из атрибутов от- носятся к типу XmlnsDefinitionAttribute, а этот класс содержит два важных свойства с именами XmlNamespace и ClrNamespace. У одного из объектов XmlnsDefinitionAttri- bute в PresentationFramework свойству XmlNamespace задана строка http://schemas. microsoft.com/winfx/2006/xaml/presentation, а свойству ClrNamespace — строка System. Windows.Controls. Синтаксис выглядит примерно так: [assembly:XmlnsDef1ni ti on ("http://schemas.microsoft.com/wi nfx/2006/xaml/presentation", "System.Wi ndows.Controls")] Другие атрибуты XmlnsDefinition ассоциируют то же пространство имен XML с пространствами имен CLR System.Windows, System.Windows.Controls.Primitives, System.Windows.Input, System.Windows.Shapes и т. д. Парсер XAML анализирует атрибуты XmlnsDefinition (если они имеются) во всех сборках, загруженных приложением. Если какие-либо пространства имен в этих атрибутах совпадают с пространством имен XML в XAML-файле, парсер знает, какие пространства имен CLR следует использовать при поиске класса Button в этих сборках. Конечно, у парсера возникнут большие проблемы, если программа содержит ссылку на другую сборку, которая содержит аналогичный атрибут XmlnsDefinition с тем же пространством имен XML, но совершенно другим классом Button. Од- нако такое может произойти только в том случае, если кто-то создаст сборку с использованием пространства имен XML от Microsoft или же разработчики Microsoft капитально оплошают. Давайте явно зададим ширину кнопки в XAML Cruncher, задавая свойст- во Width: <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml Width="144">
свойства и атрибуты 525 Hello, XAML! </Button> Используя рефлексию, парсер определяет, что объект Button действительно содержит свойство Width. Парсер также может определить, что это свойство относится к типу double или к типу CLR System.Double. Но значение Width, как и для всех атрибутов XML, представляет собой строку. Парсер должен преобра- зовать строку в объект типа double. На первый взгляд задача кажется элементар- ной. Более того, структура Double содержит статический метод Parse, предназна- ченный специально для преобразования строк в числа. Однако в общем случае такое преобразование является делом нетривиаль- ным, особенно если учесть, что ширина может задаваться и в дюймах в формате Width="1.5in" Число может быть отделено от строки «in» пробелом, а строка может быть записана как прописными, так и строчными буквами: Width="1.5 IN" Возможна экспоненциальная запись: Width="15e-lin" А также условное обозначение «Not a Number» (причем здесь важен регистр символов): Width="NaN" Некоторые атрибуты double допускают использование обозначений положи- тельной и отрицательной бесконечности Infinity и -Infinity (хотя для свойства Width это семантически некорректно). Значения могут задаваться в метрических единицах: Wldth^B.Slcm" А пользователь, обладающий опытом работы в типографском деле, предпоч- тет пункты: Width="108pt" Метод Double.Parse поддерживает научную запись, NaN и Infinity, но не строки In, cm и pt. Для них необходимо реализовать отдельную обработку. Когда парсер XAML обнаруживает свойство типа double, он ищет класс Double- Converter в пространстве имен System.ComponentModel. Это лишь один из многих классов-«конверторов». Все эти классы являются производными от TypeConverter и включают метод ConvertFromString, который в конечном счете (для данного слу- чая) использует метод Double.Parse для выполнения преобразования. Аналогично, при задании атрибута Margin (типа Thickness) парсер ищет класс ThicknessConverter в пространстве имен System.Windows. Конвертор позволяет за- дать одно значение, применяемое ко всем четырем сторонам: Marg1n="48"
526 Глава 20. Свойства и атрибуты или два значения, из которых первое применяется к левой и правой сторонам а второе — к верхней и нижней: Margiп="48 96" Числа разделяются пробелом или запятой. Также можно задать четыре числа для четырех сторон: Margiп="48 96 24 192" Если вы захотите использовать суффикс in, cm или pt, между числом и еди- ницей измерения не должно быть пробелов: Marginal. 27cm 96 18pt 2in" При вводе кода XAML в редакторе Visual Studio действуют более жесткие правила, чем для реального парсера. Если ваш код XAML им не соответствует, редактор выводит предупреждения. Так, при определении объекта Thickness Visual Studio предпочитает, чтобы значения разделялись запятыми. Для логических значений могут использоваться строки true или false с лю- бым сочетанием регистра символов: IsEnabled="FaLSe" Однако Visual Studio предпочитает True и False. Для свойств, которым задаются члены перечислений, класс EnumConverter тре- бует использовать имя члена при задании атрибута: HorizontalAlignment="Center" Как вы, возможно, помните, свойствам FontStretch, Fontstyle и FontWeight задают- ся не члены перечислений, а статические свойства классов FontStretches, Fontstyles и FontWeights. Классы FontStretchConverter, FontStyleConverter и FontWeightConverter позволяют использовать эти статические свойства напрямую. Свойству FontFamily можно задать строку, a FontSize — значение double: FontFamily="Times New Roman" FontSize="18pt" FontWeight="Bold" FontStyle="Italic" Перейдем к совершенно иной теме. Приводимый далее XAML-файл Star.xaml рисует пятиконечную звезду. Star.xaml <।_ _ ======================================= Star.xaml (с) 2006 by Charles Petzold <Polygon xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Points="144 48. 200 222, 53 114, 235 114, 88 222" Fill="Red"
свойства и атрибуты 527 Stroke="Blue" StrokeThickness="5" /> Класс Polygon содержит свойство Points типа Pointcollection. К счастью, сущест- вует класс PointCollectionConverter, который позволяет задавать точки в виде по- следовательности чередующихся координат X и Y. Эти числа разделяются про- белами или запятыми. Одни программисты разделяют координаты запятыми, а точки — пробелами. Другие (в том числе и я) разделяют точки запятыми. Конечно, класс Brushconverter позволяет задавать цвета с использованием ста- тических членов класса Brushes, но также поддерживаются шестнадцатеричные цветовые коды RGB: Fin=M#FF0000” Тот же цвет, но с альфа-каналом 128 (полупрозрачный): РП^’^ВОРГОООО" Также допускается использование дробных составляющих красного, зелено- го и синего в цветовой схеме scRGB, которым предшествует интенсивность аль- фа-канала. Полупрозрачный красный: Fill="sc#0.5,1,0,0" Вместо того чтобы задавать свойству Fill объект типа SolidColorBrush, давайте зададим ему объект LinearGradientBrush. Внезапно мы оказываемся в тупике. Как представить целый объект Linear- GradientBrush в текстовой строке, задаваемой свойству Fill? Для SolidColorBrush достаточно одного цветового значения, тогда как для градиентных кистей необ- ходимы как минимум два цвета. Здесь проявляются ограниченные возможности разметки. На самом деле задать LinearGradientBrush в XAML можно. Чтобы понять, как это делается, давайте сначала рассмотрим альтернативный синтаксис задания свойству Fill однородной красной кисти. Начнем с замены пустого тега содержи- мого объекта Polygon явно заданным конечным тегом: <Polygon xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Points="144 48, 200 222, 53 114, 235 114, 88 222" Fill="Red" Stroke="Blue" StrokeThickness="5"> </Polygon> Удалим атрибут Fill из тега Polygon и заменим его дочерним элементом с име- нем Polygon.Fill. В качестве содержимого элемента используется слово «Red»: <Polygon xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Points="144 48, 200 222, 53 114, 235 114, 88 222" Stroke="Blue"
528 Глава 20. Свойства и атрибуты StrokeThickness="5"> <Polygon.Fj 11 > Red </Polygon.Fj11> </Polygon> Давайте определимся с некоторыми терминами. Многие элементы XAML ссылаются на классы и структуры, а на их основе создаются объекты. Такие эле- менты называются объектными элементами: <Polygon ... /> Довольно часто элемент содержит атрибуты, задающие свойства этих объек- тов. Они называются атрибутами свойств'. Fill^'Red” Также можно задать свойство в альтернативном синтаксисе, в котором за- действован дочерний элемент. Такие элементы называются элементами свойств: <Polygon.Fi П> Red </Polygon.Fi11> Элементы свойств можно отличить по точке, отделяющей имя элемента от имени свойства. Они не обладают атрибутами. Например, следующая запись не- возможна ни при каких условиях: <!-- Wrong Syntax! --> <Polygon.Fill SomeAttri bute="Whatever"> </Polygon.Fi11 > Здесь не могут находиться даже объявления пространств имен XML. При попытке задания атрибута для элемента свойства XAML Cruncher выдает сооб- щение: «Cannot set properties on property elements» — это сообщение исключе- ния, которое инициируется методом XamIReader.Load при нарушении указанного условия. Элементы свойств должны обладать содержимым, преобразуемым к типу свойства. Так, элемент свойства Polygon.Fill ссылается на свойство Fill, относяще- еся к типу Brush, поэтому его содержимое должно преобразовываться в Brush: <Polygon.Fi11> Red </Polygon.Fi11> Этот синтаксис тоже работает: <Polygon.Fi11> #FFOOOO </Polygon.Fi11> Чтобы более очевидно указать на принадлежность Polygon.Fill к типу Brush, можно использовать следующий (менее компактный) синтаксис:
Сяпйства и атрибуты 529 <Polygon.Fill> <Brush> Red </Brush> </Polygon.Fill> Теперь содержимое элемента свойства Polygon.Fill представляет объект Brush. Строка Red преобразуется в объект типа SolidColorBrush, поэтому элемент свойст- ва Polygon.Fill можно записать в следующем виде: <Polygon.Fill> <SolidColorBrush> Red </SolidColorBrush> </Polygon.Fill> Класс SolidColorBrush содержит свойство с именем Color; а класс ColorConverter поддерживает те же преобразования, что и конвертор Brush, поэтому свойство Color класса SolidColorBrush можно задать с использованием атрибута свойства: <Polygon.Fi11> <Sol1dColorBrush Color="Red"> </SolidColorBrush> </Polygon.Fi11> Однако заменить SolidColorBrush на Brush нельзя, потому что Brush не имеет свойства с именем Color. Так как элемент SolidColorBrush не имеет содержимого, тег можно записать в синтаксисе пустого элемента: <Polygon.Fj11> <SolidColorBrush Color="Red" /> </Polygon.Fi11> А можно выделить свойство Color класса SolidColorBrush в синтаксисе элемента свойства: <Polygon.Fi11> <Soli dColorBrush> <Soli dColorBrush.Color> Red </So1i dColorBrush.Color> </SolidColorBrush> </Polygon.Fi11> Свойство Color класса SolidColorBrush представляет собой объект типа Color, поэтому для содержимого SolidColorBrush.Color можно явно использовать объект- ный элемент: Polygon. Fi 11 > <SolidColorBrush> <So1i dColorBrush.Color>
530 Глава 20. Свойства и атрибуты Colог> Red </Colог> </SolidColorBrush.Color> </Sol1dColorBrush> </Polygon.Fj11> Как говорилось ранее, Color обладает свойствами A, R, G и В типа byte. Эти свойства можно задать в теге Color с использованием десятеричной или шестна- дцатеричной записи: <Polygon.Fill> <Sol1dColorBrush> <So11dColorBrush.Color> Color A="255" R="#FF" G=,,0" B=,,0”> </Color> </Soli dColorBrush.Color> </SolidColorBrush> </Polygon.Fi11> Помните, что эти четыре свойства нельзя задать в теге SolidColorBrush.Color, поскольку, как указано в сообщении исключения, «свойства нельзя задавать для элементов свойств». Элемент Color не имеет содержимого, поэтому его можно записать в синтаксисе пустого элемента: <Polygon.Fj11> <SolidColorBrush> <So11dColorBrush.Color> Color A="255" R="#FF" G=,,0" B=,,0" /> </SolidColorBrush.Color> </SolidColorBrush> </Polygon.Fi11> Также можно выделить один пли несколько атрибутов Color: <Polygon.Fi11> <SolidColorBrush> <So1i dColorBrush.Color> Color A="255" G="0" B="0"> Color. R> #FF </Color.R> </Color> </So1i dColorBrush.Col or> </SolidColorBrush> </Polygon.Fi 11 > Свойство R структуры Color относится к типу Byte (структура, определяемая в пространстве имен System); вы даже можете включить в XAML элемент Byte, чтобы тип данных R стал еще более очевидным. Однако пространство имен System не входит в число пространств имен CLR, ассоциируемых с двумя пространствами имен XML в начале XAML-файла.
пюйства^и атрибуты 531 Чтобы сослаться на структуру Byte в XAML-файле, необходимо добавить еще одно объявление пространства имен XML. Давайте свяжем пространство имен System с префиксом s: xml ns:s=”clr-namespace:System:assembly=mscorli b" Обратите внимание: строка в кавычках начинается с префикса clr-namespace, за которым следует двоеточие и пространство имен CLR, как если бы вы на- значали префикс для пространства имен CLR в своей собственной программе (см. проект UseCustomClass в главе 19). Поскольку классы и структуры простран- ства имен System находятся во внешней сборке, необходимо указать эту инфор- мацию. За пространством CLR следует точка с запятой, слово assembly, знак ра- венства и имя сборки. Предполагается, что начальная часть строки до двоеточия является аналогом префикса http: в традиционном объявлении пространства имен. Это объявление должно располагаться в самом элементе Byte или в его роди- теле. Поместим его в элемент Color (по причинам, которые вскоре станут ясны): <Polygon.Fill> <Sol1dColorBrush> <So11dColorBrush.Color> <Color xml ns:s="clr-namespace:System;assembly=mscorlib" A="255" G="0" B=,,0"> <Color.R> <s:Byte> #FF </s:Byte> </Color.R> </Color> </SolidColorBrush.Color> </SolidColorBrush> </Polygon.F111> Можно пойти на крайность и выделить все четыре свойства Color: <Polygon.Fi11> <Sol1dColorBrush> <So11dColorBrush.Color> <Color xml ns:s=”clr-namespace:System;assembly=mscorl1b"> <Color.A> <s:Byte> 255 </s:Byte> </Color.A> <Color.R> <s:Byte> 255 </s:Byte> </Color,R> <Color.G>
532 Глава 20. Свойства и атрибуту <s:Byte> О </s:Byte> </Color.G> <Color.В> <s:Byte> О </s:Byte> </Color,B> </Color> </So11dColorBrush.Color> </SolidColorBrush> </Polygon.Fi11> Такое решение не сработало бы, если бы объявление пространства имен рас- полагалось в первом элементе Byte. Объявление распространяется на тот эле- мент, в котором оно находится, а также на все вложенные элементы. Кажется, нам удалось сделать код XAML гораздо более объемистым, чем это реально необходимо. Однако данный синтаксис демонстрирует принцип опре- деления градиентной кисти в качестве свойства Fill. Класс LinearGradientBrush содержит два свойства с именами StartPoint и EndPoint. По умолчанию их значения задаются в координатной системе относительно окра- шиваемого объекта. Третье важное свойство Gradientstops (тип GradientStopCollec- tion) содержит коллекцию объектов Gradientstop, обозначающих переходные цвета. Элемент свойства Polygon.Fill должен обладать содержимым типа Brush. Объ- ектный элемент типа LinearGradientBrush удовлетворяет этому критерию: <Polygon.Fi11> LinearGradientBrush ...> </LinearGradientBrush> </Polygon.Fi11> Свойства StartPoint и EndPoint достаточно просты, чтобы их можно было опре- делить как атрибуты свойств в начальном теге LinearGradientBrush: <Polygon.Fi11> LinearGradientBrush StartPoint="0 0" EndPoint="l 0"> </L inearGradi entBrush> </Polygon.Fi11> Однако свойство Gradientstops должно стать элементом свойства: <Polygon.Fi11> LinearGradientBrush StartPoint="0 0" EndPoint="l 0"> LinearGradi entBrush.Gradi entStops> </L inearGradi entBrush.Gradi entStops> </L1nearGradi entBrush> </Polygon.Fill>
Свойства и атрибуты 533 Свойство Gradientstops относится к типу GradientStopCollection, поэтому мы со- здаем объектный элемент для этого класса: <Polygon.Fl11> LinearGradientBrush StartPo1nt="0 0" EndPo1nt="l 0"> <L1nearGradlentBrush.GradientStops> <Grad1entStopCol1ectlon> </G ra d1entStopCol1ect1on> </L1nearGradlentBrush.GradientStops> </L1nearGradlentBrush> </Polygon.F111> Класс GradientStopCollection реализует интерфейс IList; этого факта достаточно, чтобы его члены можно было записывать в виде простых дочерних элементов. Обратите внимание, как удобно записывается каждый элемент Gradientstop в син- таксисе пустого элемента: <Polygon.F111> <L1nearGradlentBrush StartPo1nt="0 0" EndPo1nt="l 0"> <Grad1entStopCol1ectlon> <Grad1 entStop 0ffset="0" Color="Red" /> <Grad1entStop 0ffset=”0.5“ Color="Green" /> <Grad1entStop Offset=“l“ Color="Blue" /> </G ra d1entStopCol1ect1on> </L1nearGradlentBrush> </Polygon.Fl 11> Впрочем, запись можно сделать еще проще. Явное присутствие элемента свой- ства LinearGradientBrush.GradientStops или объектного элемента GradientStopCollection не обязательно, и их можно исключить: <Polygon.Fl 11> <L1 nearGradlentBrush StartPo1nt="0 0" EndPo1nt="l 0"> <Grad1entStop Offset="0" Color="Red" /> <Grad1entStop 0ffset="0.5" Color="Green" /> <Grad1entStop Offset=‘T' Color="Blue" /> </L1nearGradlentBrush> </Polygon.Fl 11> Вот и все! Следующая программа рисует звезду с радиальной градиентной закраской. RadialGradientStar.cs <! -. ===================================================== RadlalGradlentStar.xaml (с) 2006 by Charles Petzold <Polygon xmlns="http:// schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml"
534 Глава 20. Свойства и атрибуты Points="144 48, 200 222, 53 114, 235 114, 88 222" Stroke="Blue" StrokeThickness="5"> <Polygon.Fi 11 > <Radi alGradl entBrush> <Rad1 a1GradlentBrush.Gradi entStops> <GradientStop 0ffset=“0“ Color="Blue" /> <GradientStop 0ffset="l" Color="Red" /> </Rad1alGradlentBrush.GradlentStops> </Rad1 alGradlentBrush> </Polygon.Fi11 > </Polygon> Но вернемся к объектам Button. В XAML-файле три свойства Button, в том числе и свойство Content, задаются в виде атрибутов: <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Foreground="LightSeaGreen" FontSize="24 pt" Content="Hello, XAML!"> </Button> Атрибут Foreground можно преобразовать в элемент свойства: <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" FontSize="24 pt" Content="Hello. XAML!"> <Button.Foreground> LightSeaGreen </Button.Foreground> </Button> Преобразование атрибута FontSize в элемент свойства производится так: <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Foreground="LightSeaGreen" Content="Hello, XAML!"> <Button.FontSize> 24 pt </Button.FontSize> </Button> Наконец, и Foreground, и FontSize могут быть элементами свойств: <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Content="Hello, XAML!"> <Button.Foreground> LightSeaGreen
свойства и атрибуты 535 </Button. Foreground <Button.FontSize> 24 pt </Button.FontSize> </Button> Свойство Content тоже можно оформить в виде элемента свойства: <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml" Foreground="LightSeaGreen" FontSize="24 pt"> <Button. Contend Hello, XAML! </Button. Contend </Button> В этом случае теги Button.Content не обязательны: <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml" Foreground="LightSeaGreen" FontSize="24 pt"> Hello, XAML! </Button> Содержимое может чередоваться с элементами свойств. Я вставил пустые строки в следующий XAML-файл, чтобы он лучше читался. <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Button.Foreground> LightSeaGreen </Button.Foreground> Hello, XAML! <Button.FontSize> 24 pt </Button.FontSize> </Button> Каждое свойство Button либо интерпретируется как атрибут, задаваемый в на- чальном теге Button, либо для него используется элемент свойства, у которого значение оформляется в виде дочернего элемента. Исключение составляет свой- ство Content. Его значение можно просто оформить в качестве дочернего элемен- та Button без использования свойств элементов. Чем же Content отличается от других свойств?
536 Глава 20. Свойства и атрибуты Каждый класс, используемый с XAML, теоретически может обладать одним свойством, которое определяется как свойство содержимого. Для класса Button им является свойство Content. Свойство содержимого назначается в определении класса при помощи атрибута ContentPropertyAttribute (пространство имен System. Windows.Serialization). Определение класса Button в исходном коде Presentation- Framework.dll выглядит примерно так: [Contentproperty("Content")] public class Button: ButtonBase { } Иначе говоря, класс Button просто наследует атрибут Contentproperty от Content- Control. С другой стороны, класс StackPanel определяется с атрибутом Contentproperty, который выглядит так: [Contentproperty("Chi 1dren")] public class StackPanel: Panel { } Атрибут ContentProperty дает возможность включать дочерние объекты Stack- Panel в виде дочерних элементов элемента StackPanel: <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Button Horizontal Alignment="Center"> Button Number One </Button> <TextBlock Horizontal Alignment="Center"> TextBlock in the middle </TextBlock> <Button Horizontal Alignment="Center"> Button Number One </Button> </StackPanel> Свойством содержимого LinearGradientBrush и RadialGradientBrush является свой- ство Gradientstops. Свойством содержимого класса TextBlock является свойство Inlines (коллек- ция объектов Inline). Класс Run наследует от Inline, и свойством содержимого Run является Text — объект типа string. Все эти обстоятельства предоставляют боль- шую свободу при определении содержимого TextBlock: <TextBlock> This is <Italic>italic</Italic> text and this is <Bold>bold</Bold> text </TextBlock>
гяпйства и атрибуты 537 Свойства содержимого классов Italic и Bold относятся к типу Inline. Этот фраг- мент XAML можно записать с явным указанием свойства Text, наследуемого классами Italic и Bold от Span: <TextBlock> This is <Italic Text="italic" /> text and this is <Bold Text="bold" /> text </TextBlock> Так как свойства содержимого играют важную роль при записи XAML, было бы полезно иметь список всех классов, для которых определен атрибут Content- Property, и самих свойств содержимого. Приведенная далее консольная програм- ма выдает эту информацию. DumpContentPropertyAttributes.cs // II DumpContentPropertyAttributes.cs (с) 2006 by Charles Petzold // using System; using System.Col 1ections.Generic; using System.Reflection; using System.Windows: using System.Windows.Markup: using System.Windows.Navigation: public class DumpContentPropertyAttributes [STAThread] public static void MainO { // Обеспечить загрузку библиотек PresentationCore // и PresentationFramework UIEIement dummyl = new UIElementO: FrameworkElement dummy2 = new FrameworkElement(): // Объект SortedList для хранения классов и свойств содержимого SortedList<string, str1ng> listclass = new SortedList<string. stri ng>(); // Форматная строка string strFormat = "{0,-35}{1}"; // Перебор загруженных сборок foreach (AssemblyName asmblyname in Assembly.GetExecutingAssembly().GetReferencedAssemblies()) { // Перебор типов foreach (Type type in Assembly.Load(asmblyname).GetTypesO)
538 Глава 20. Свойства и атрибуты // Перебор пользовательских атрибутов // (используйте аргумент ’false'. чтобы ограничить // поиск ненаследуемыми атрибутами!) foreach (object obj In type.GetCustomAttributes( typeof(ContentPropertyAttribute), true)) { // Если это ContentPropertyAttribute. включить в список if (type.IsPublic && obj as ContentPropertyAttribute != null) 1istClass.Add(type.Name. (obj as ContentPropertyAttribute).Name): } } // Вывод результатов ‘ Console.WriteLine(strFormat, "Class". "Content Property"); Console.WriteLine(strFormat, "-----". "----------------"): foreach (string strClass in 1IstClass.Keys) Console.WriteLine(strFormat. strClass. 11stClassCstrClass]): } } Некоторые элементы — и прежде всего DockPanel и Grid — обладают присо- единенными свойствами, которые используются в коде C# в синтаксисе сле- дующего вида: DockPanel.SetDock(btn, Dock.Top); Эта команда означает, что элемент btn должен быть пристыкован у верхнего края DockPanel. Если btn не является дочерним объектохм по отношению к Dock- Panel, вызов ни к чему не приведет. Как было показано в главе 8, вызов статиче- ского метода SetDock эквивалентен следующему вызову: btn.SetValue(DockPanel.DockProperty. Dock.Top); В XAML используется синтаксис следующего вида: <Button DockPanel.Dock="Top" ... /> Далее приводится небольшой автономный XAML-файл, немного напоминаю- щий программу DockAroundTheBlock из главы 6. AttachedPropertiesDemo.xaml <।_ _ ========================================================= AttachedPropertiesDemo.xaml (с) 2006 by Charles Petzold <DockPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation” xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Button Content="Button No. 1" DockPanel.Dock="Left" />
Свойства и атрибуты 539 <Button Content="Button No. 2" <Button Content="Button No. 3" <Button Content="Button No. 4" <Button Content="Button No. 5" DockPanel.Dock="Top" /> DockPanel.Dock="Right" /> DockPanel.Dock="Bottom" /> /> </DockPanel> Объект Grid особенно эффективен в XAML, потому что определения строк и столбцов гораздо компактнее своих аналогов в коде С#. Каждое определение RowDefinition и ColumnDefinition занимает всего одну строку в свойствах элементов Grid.RowDefinitions и Grid.ColumnDefinitions. SimpleGrid.xaml SlmpleGrid.xaml (c) 2006 by Charles Petzold <Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Grid.RowDefinitions> <RowDefinition Height="100" /> <RowDefinition Height="Auto" /> <RowDefinition Height="100*" /> </Gri d.RowDefi ni ti ons> <Gri d.ColumnDefi ni ti ons> <ColumnDefinition Width="33*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="67*” /> </Gr1d.ColumnDefi ni ti ons> <Button Content="But.ton No. <Button Content="Button No. <Button Content="Button No. 1" Grid.Row=,,0" Grid.Column="0" /> 2" Grid.Row="U Grid.Column="0" /> 3" Grid.Row="2" Grid.Column="0" /> <Gri dSpl i tter Horizontal Al ignment="Center" Width=,,6" Grid.Row="0" Grid.Colитп="Г' Grid.RowSpan="3" /> <Button Content="Button No. 4" Grid.Row="0" Grid.Column="2" /> <Button Content="Button No. 5" Grid.Row="r Grid.Column="2" /> <Button Content="Button No. 6" Grid.Row="2" Grid.Column="2" /> </Grid> Если вас устраивают принятые по умолчанию значения Height и Width 1* (в синтаксисе XAML), элементы RowDefinition и ColumnDefinition даже можно за- писать в виде <RowDefinition /> Присоединенные свойства — не единственные атрибуты, в которых могут со- держаться точки. Вы также можете определить свойства с атрибутами, в кото- рых перед именем свойства указывается имя класса. Имя класса может совпадать
540 Глава 20. Свойства и атрибуты с именем элемента, в котором находится атрибут, но также может быть именем класса-предка или класса, также являющегося владельцем того же зависимого свойства. Например, все следующие атрибуты являются допустимыми для эле- мента Button: Button.Foreground="Blue" TextBlock.FontSize="24pt" FrameworkElement.Horj zontalAlj gnment="Center" ButtonBase.Vertical Alignment="Center" UIEIement.0pacity="0.5" В некоторых случаях возможно использование атрибутов с именем класса и свойством в элементе, в котором это свойство не определяется. Пример: Propertylnheritance.xaml <।_ _ ====================================================== Propertylnheritance.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Horizontal Alignment="Center" TextBlock.FontSize="16pt" TextBlock.Foreground="Blue" > <TextBlock> Just a TextBlock </TextBlock> <Button> Just a Button </Button> </StackPanel> Свойства FontSize и Foreground задаются в элементе StackPanel — классе, не об- ладающем этими свойствами, поэтому перед именами свойств должно быть за- дано имя класса, определяющего свойства. В результате назначения атрибутов и TextBlock, и Button назначается размер шрифта 16 пунктов, но по какой-то не- объяснимой странности синий основной цвет назначается только TextBlock. Также возможно использовать атрибут с именем класса и события, чтобы установить обработчик для маршрутизируемого события. Действие обработчика распространяется на все дочерние элементы. Проект RoutedEventDemo содер- жит два файла, RoutedEventDemo.xaml и RoutedEventDemo.cs. В элементе ContextMenu XAML-файла содержится атрибут MenuItem.Click. Этот обработчик применяется к элементам Menuitem, составляющим контекстное меню TextBlock. RoutedEventDemo.xaml <I_ _ ================================================== RoutedEventDemo.xaml (с) 2006 by Charles Petzold
Свойства и атрибуты 541 <Window xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Class="Petzold.RoutedEventDemo.RoutedEventDemo" Title="Routed Event Demo"> <TextBlock Name="txtblk" FontSize="24pt" Horizontal Alignment="Center" Vertical Alignment="Center" ToolTip="Right click to display context menu"> TextBlock with Context Menu <TextBlock.ContextMenu> <ContextMenu Menu Item.Click="MenuItemOnClick"> <ContextMenu> <MenuItem Header="Red" /> <MenuItem Header="Orange" /> <MenuItem Header="Yellow" /> <MenuItem Header="Greem" /> <MenuItem Header="Blue" /> <MenuItem Header="Indigo" /> <MenuItem Header="Violet" /> </ContextMenu> </TextBlock.ContextMenu> </TextBlock> </Window> Обработчик события содержится в файле С#. Объект Menuitem, инициировав- ший событие, определяется свойством Source объекта RoutedEventArgs. Обратите внимание на использование статического метода ColorConverter.ConvertFromString для преобразования текста Menuitem в объект Color. RoutedEventDemo.cs // // RoutedEventDemo.cs (с) 2006 by Charles Petzold // using System; using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: namespace Petzold.RoutedEventDemo public partial class RoutedEventDemo : Window { [STAThread] public static void MainO { Application app = new Application!);
542 Глава 20. Свойства и атрибуты app.Run(new RoutedEventDemoO); } public RoutedEventDemoO { InitializeComponentO: } void MenuItemOnClick(object sender. RoutedEventArgs args) { string str = (args.Source as Menuitem).Header as string; Color clr = (Color)ColorConverter.ConvertFromString(str): txtblk.Foreground = new SolidColorBrush(clr): ) } } Мы рассмотрели несколько ситуаций, в которых элемент или атрибут может содержать точку. Далее приводится краткая сводка. О Если имя элемента не содержит точки, оно всегда представляет имя класса или структуры: <Button ... /> О Если имя элемента содержит точку, то оно состоит из имени класса или структуры, за которым следует имя свойства этого класса/структуры. Такой элемент является элементом свойства: <Button.Background> </Button.Background> О Этот конкретный элемент должен быть дочерним по отношению к Button, а на- чальный тег не может содержать атрибутов. Содержимое элемента должно преобразовываться к типу свойства (например, строка "Red" или "#FFOOOO”) или дочернему элементу, тип которого соответствует типу свойства (напри- мер, Brush или любой класс, производный от Brush). О Имена атрибутов обычно не содержат точек: < ... Background="Red" ... > О Такие атрибуты соответствуют свойствам элементов, в которых они находят- ся. Если атрибут содержит точку, возможно, он представляет присоединен- ное свойство: < ... DockPanel.Dock="Left" ... > О Как правило, данное присоединенное свойство встречается в дочерних объ- ектах DockPanel, но это не обязательно. Если присоединенное свойство встре- чается в элементе, не являющемся дочерним по отношению к DockPanel, оно просто игнорируется. О Атрибут с точкой также может представлять маршрутизируемое событие ввода: < ... MenuItem.Click="MenuItemOnClick" ... >
Свойства и атрибуты 543 Вероятно, такой атрибут встретится не в элементе Menuitem (потому что в этом случае он бы назывался просто Click), а в элементе с несколькими дочерними элементами Menuitem. Точки также могут отображаться в определениях свойств, предназначенных для наследования: < ... TextBlock.FontSize="24pt" ... > Пожалуй, вы согласитесь с тем, что XAML во многих случаях компактнее эквивалентного кода C# и лучше подходит для представления иерархических структур, встречающихся при формировании макета окна с панелями и эле- ментами. Но разметка в общем случае обладает гораздо более ограниченными возможностями по сравнению с процедурными языками, потому что в ней от- сутствует концепция передачи управления. Но как будет показано в следующей главе, некоторые возможности XAML компенсируют эти недостатки.
Ресурсы Предположим, вы пишете код XAML для обычного или диалогового окна. При оформлении элементов будут использоваться шрифты двух размеров. Вероятно, вы уже знаете, какие элементы будут оформлены большим или меньшим шриф- том, но еще не уверены в точных значениях размеров (вам хочется немного по- экспериментировать перед тем, как выбрать тот или иной размер). Тривиальное решение заключается в жестком кодировании размеров FontSize в XAML: FontSize='T4pt" Если позднее шрифт потребуется увеличить или уменьшить, достаточно вы- полнить поиск с заменой. Однако любой программист знает, что поиск с за- меной работает в малом масштабе, но не является общим решением проблем такого рода. А если вместо простого размера шрифта речь идет о сложной гради- ентной кисти? Определение градиентной кисти можете копировать и вставлять, но если вам когда-либо понадобится изменить ее, правку придется вносить во многих местах. Столкнувшись с подобной проблемой в С#, вы бы не стали дублировать код или жестко кодировать значения размеров. Для таких объектов определяются переменные или (для наглядности и повышения эффективности) константные поля в классе окна: const double fontsizeLaгде =14/0.75: const double fontsizeSmall =11/0.75: Также можно определить их в виде статических значений, доступных только для чтения: static readonly double fontsizeLarge = 14 / 0.75: static readonly double fontsizeSmall =11/0.75: К счастью, в XAML существует свой аналог переменных. Чтобы заново ис- пользовать объекты в XAML, следует сначала определить их как ресурсы. Ресурсы, о которых пойдет речь в этой главе, не имеют ничего общего с ре- сурсами, упоминавшимися ранее. В предыдущих главах я показывал, как при помощи Microsoft Visual Studio откомпилировать некоторые файлы, включен- ные в проект, в режиме ресурсов (свойство Build Action равно Resource).
Ресурсы 545 Вероятно, такие ресурсы было бы точнее называть ресурсами сборок. Чаще всего ресурсы сборок представляют собой двоичные файлы (например, значки и растровые изображения). Ресурсы сборок хранятся в сборках (исполняемых файлах или библиотеках динамической компоновки), а для обращения к ним определяется объект Uri, ссылающийся на исходное имя файла ресурса. Ресурсы этой главы иногда называются локальными ресурсами, потому что они определяются в XAML (или иногда в коде С#) и ассоциируются с интер- фейсным элементом, элементом управления, страницей или окном приложения. Конкретный ресурс доступен только в границах элемента, в котором он опре- деляется, и в его дочерних элементах. Рассматривайте ресурсы как своего рода XAML-аналог статических полей С#, доступных только для чтения. Как и ста- тические поля, ресурсные объекты создаются один раз во время запуска и со- вместно используются элементами, которые на них ссылаются. Ресурсы хранятся в объекте типа ResourceDictionary. Три важнейших класса — FrameworkElement, FrameworkContentElement и Application — содержат свойство Resou- rces типа ResourceDictionary. Каждый ресурс хранится в ResourceDictionary с клю- чом, предназначенным для идентификации объекта. Обычно в качестве ключей используются текстовые строки. XAML определяет атрибут х:Кеу специально для определения ключей ресурсов. Любой элемент, производный от FrameworkElement, может содержать коллек- цию Resources. Секция Resources определяется в синтаксисе элементов свойств в самом начале элемента: <StackPanel> <StackPanel.Resources> </StackPanel.Resources> </StackPanel> Ресурсы, определяемые в секции Resources, могут использоваться в StackPanel и во всех дочерних элементах StackPanel. Каждый ресурс в секции Resources за- писывается в следующей форме: <SomeType x:Key="mykey" ...> </SomeType> Для задания свойств объекта может использоваться либо синтаксис атрибу- тов, либо синтаксис элементов свойств. Элементы XAML обращаются к ресур- сам по ключу с использованием расширения разметки — специального ключево- го слова, определяемого для использования в XAML. Расширение разметки для ресурсов называется StaticResource. Я начал эту главу с описания проблемы двух шрифтов. Следующий авто- номный XAML-файл показывает, как определить два соответствующих ресурса в секции Resources объекта StackPanel и как затем обращаться к ним из дочерних элементов StackPanel.
546 Глава 21. Ресурсы FontSizeResources.xaml <I_ _ ==================================================== FontSizeResources.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:s="clr-namespace:System;assembly=mscorli b"> <StackPanel.Resources> <s:Double x:Key="fonts!zeLarge"> 18.7 </s:Double> <s:Double x:Key="fontsizeSmal 1"> 14.7 </s:Double> </StackPanel.Resources> <Button HorizontalAlignment="Center" Vertical Alignment="Center" Margin="24"> <Button.FontSize> <StaticResource ResourceKey="fontsizeLarge" /> </Button.FontSize> Button with large FontSize </Button> <Button Horizontal Alignment="Center" Vertical Alignment="Center" Margin="24" FontSize="{StaticResource fonts!zeSmall}" > Button with small FontSize </Button> </StackPanel> Тег элемента StackPanel определяет префикс пространства имен XML s для пространства System (clr-namespace:System;assembly=mscorlib) для ссылок на струк- туру Double в коллекции Resources. В секции Resources объекта StackPanel определяются два объекта Double с клю- чами «fontsizeLarge» и «fontsizeSmall». Ключи в пределах отдельного словаря должны быть уникальными. Значения 18,7 и 14,7 обозначают 14 и И пунктов соответственно. StackPanel и дочерние элементы StackPanel могут использовать ресурсы одним из двух способов, в каждом из которых задействовано расширение разметки StaticResource. Первый элемент Button обращается к ресурсу с использованием синтаксиса элемента свойства FontSize с элементом StaticResource и атрибута ResourceKey, обозначающего ключ: <Button.FontSize> <StaticResource ResourceKey="fontsizeLarge" /> </Button.FontSize>
ресурсу____________________________________________________________________ Более распространенный вариант синтаксиса показан для второго элемента Button. Атрибуту FontSize задается строка, содержащая слово StaticResource и имя ключа в фигурных скобках: FontSize="{StaticResource fontsizeSmall}" Присмотритесь к синтаксису повнимательнее: — он очень часто встречается в этой главе (а также в главе 23, когда речь пойдет о привязке данных). Фигур- ные скобки означают, что находящееся в них выражение является расшире- нием разметки. Класса с именем StaticResource не существует. Впрочем, сущест- вует класс StaticResourceExtension, производный от MarkupExtension и содержащий свойство ResourceKey. StaticResource как расширение разметки позволяет сде- лать в XAML нечто такое, что обычно возможно только в процедурном коде. По сути класс StaticResourceExtension предоставляет значение из словаря на осно- вании заданного ключа. В этой главе мы встретимся еще с двумя расширениями разметки: x:Static и DynamicResource, которые тоже заключаются в фигурные скобки. Присутствие фигурных скобок сообщает парсеру XAML о присутствии расширения размет- ки. Никакие дополнительные кавычки или апострофы в фигурных скобках не- допустимы. В редких случаях требуется начать с фигурных скобок текстовую строку, не связанную с расширением разметки: <!-- Не работает! --> <TextBlock Text="{just a little text in here}" /> Чтобы парсер XAML не искал (безуспешно) расширение разметки с име- нем just, вставьте в начало строки служебную последовательность из пустой пары круглых скобок: <!-- Работает нормально! --> <TextBlock Text="{}{just a little text in here}" /> Секция Resources почти всегда определяется в самом начале элемента, пото- му что ресурс должен определяться в файле до первого обращения к нему. Хотя все ключи отдельной коллекции Resources должны быть уникальными, в двух разных коллекциях Resources могут использоваться одинаковые ключи. Поиск ресурса начинается с коллекции Resources элемента, обращающегося к ре- сурсу, а затем продолжается по дереву до тех пор, пока ключ не будет найден. Следующий автономный XAML-файл демонстрирует этот процесс. ResourceLookupDemo.xaml < j - _ ===================================================== ResourceLookupDemo.xaml (с) 2006 by Charles Petzold <StackPanel xml ns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Orientation="Horizontal">
548 Глава 21. Ресурсы <StackPanel.Resources> <Sol1dColorBrush x:Key="brushText" Color="Blue" /> </StackPanel.Resources> <StackPanel> <StackPanel.Resources> <SolidColorBrush x:Key="brushText" Color="Red" /> </StackPanel.Resources> <Button HorizontalAlignment="Center" Vertical Alignment="Center" Margin="24" Foreground="{StaticResource brushText}"> Button with Red text </Button> </StackPanel> <StackPanel> <Button HorizontalAlignment="Center" Vertical Alignment="Center" Margin="24" Foreground="{StaticResource brushText}"> Button with Blue text </Button> </StackPanel> </StackPanel> В файле определяются три элемента StackPanel. Первый обладает горизон- тальной ориентацией; два других элемента StackPanel являются дочерними по отношению к нему. Для простоты каждый из двух объектов StackPanel содержит всего одну кнопку. Родительский элемент StackPanel содержит коллекцию Resources, определяю- щую объект SolidColorBrush синего цвета с ключом «brushText». Первый дочерний элемент StackPanel тоже содержит коллекцию Resources с объектом SolidColorBrush, которому назначен ключ «brushText», но на этот раз красного цвета. Обе кноп- ки задают свои свойства Foreground с расширением StaticResource, содержащим ссылку на ключ «brushText». Первая кнопка (в объекте StackPanel с красной кистью) содержит красный текст. У второй кнопки нет ресурса с именем «brushText», поэтому она исполь- зует ресурс родительского элемента StackPanel. Ее текст окрашивается в синий цвет. Определение одноименных ресурсов открывает много полезных возможно- стей, особенно при работе со стилями (см. главу 24). Стили позволяют опреде- лять свойства для нескольких элементов и даже определять реакцию этих эле- ментов на определенные события и изменения свойств. В реальных программах WPF коллекции Resources чаще всего используются для определения и переоп- ределения стилей.
pecyPcbl 549 Ресурсы используются совместно. Для каждого ресурса создается только один объект, и даже он не будет создан при отсутствии обращений к ресурсу. Можно ли определить элемент управления пли интерфейсный элемент в ка- честве ресурса? Да, можно. Например, следующий фрагмент можно включить в одну из секций Resources файла ResourceLookupDemo.xaml: <Button x:Key="btn" FontSize="24"> Resource Button </Button> Далее объект Button можно использовать в качестве дочернего элемента Stack- Panel (пли дочернего объекта StackPanel в зависимости от того, где был опреде- лен ресурс) с использованием следующего синтаксиса: <StaticResource ResourceKey="btn" /> Такое решение работает, но только один раз. Объект Button, созданный в ка- честве ресурса, если он является дочерним объектом панели, уже не сможет быть другим дочерним объектом той же или другой панели. Также обратите внима- ние, что вы не можете ничего изменить в объекте Button при обращении к нему из элемента StaticResource. Выходит, преобразование Button в ресурс ничего не дает. Зачем тогда это нужно? Если вам кажется, что элементы и интерфейсные элементы необходимо оп- ределить как ресурс, с большой вероятностью ресурс предполагается использо- вать для определения некоторых, по не всех свойств элемента. Скорее всего, на самом деле вам нужен стиль; мы вернемся к этой теме в главе 24. Хотя ресурсы почти всегда определяются в XAML, а не в процедурном коде, для включения объекта в коллекцию Resources элемента можно воспользоваться кодом C# следующего вида: stack.Resources.Add("brushText". new SolIdColorBrush(Colors.Blue)): Каждый из трех основных классов, определяющих коллекцию Resources (Fra- meworkElement, FrameworkContentElement и Application), также определяет метод Find- Resource, предназначенный для поиска ресурса с заданным ключом. Несомненно, этот метод используется StaticResourceExtension для поиска ресурсов. Интересная подробность: если вызов FindResource для определенного элемен- та находит ресурс в коллекции Resources этого элемента, поиск на этом не за- канчивается. Метод также может найти ресурс с заданным ключом в одном из предков элемента в дереве. Вероятно, реализация FindResource выглядит при- мерно так: public object FindResource(object key) object obj = Resourceslkey]: if (obj != null) return obj;
550 Глава 21. Ресурсы if (Parent != null) return Parent.FindResource(key); return Appl1 cation.Current.Fi ndResource(key); } Именно благодаря рекурсивному поиску по дереву элементов метод Find- Resource становится гораздо более ценным инструментом, чем простое индекси- рование свойства Resources по ключу. Также обратите внимание, что когда дерево элементов будет исчерпано, FindResource проверяет коллекцию Resource объекта Application. Коллекция Resources объекта Application может (и должна) использо- ваться для хранения настроек, стилей и тем уровня приложения. До настоящего момента я рекомендовал создавать в Visual Studio пустые проекты, чтобы вы лучше понимали WPF, не отвлекаясь на код, автоматически генерируемый Visual Studio. Вероятно, после знакомства с ресурсами мы можем позволить Visual Studio повлиять на ваш стиль программирования — хотя бы в экспериментальных целях. Создайте в Visual Studio проект типа Windows Presentation Foundation Applica- tion, присвойте ему имя GradientBrushResourceDemo. Visual Studio создает файл MyApp.xaml с уже определенной секцией Resources. MyApp.xaml <Арр1ication х:Class="GradientBrushResourceDemo.MyApp" xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri^'Windowl .xaml" <Application.Resources> </App1i cation.Resources> </Application> Воспользуемся секцией Resources для определения градиентной кисти уров- ня приложения. Файл MyApp.xaml приобретает следующий вид. MyApp.xaml <Application х:Class="GradientBrushResourceDemo.MyApp" xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Sta rtupllri=" Wi ndowl. xaml" <App1i cation.Resources> <LinearGradientBrush x:Key="brushGradient" StartPoint="0, 0" EndPoint="l, Г> <Li nearGradi entBrush.Gradi entStops> <GradientStop Offset="0" Color="Black" /> <GradientStop 0ffset="0.5" Color='Green" />
ресурсы 551 <Grad1entStop Offset=’T' Color="Gold" /> </L1nearGradlentBrush.GradientStops> </L1nearGradlentBrush> </App11 cation.Resources> </Appl1cat1on> Visual Studio также создает для MyApp.xaml файл программной логики МуАрр. xarnl.cs, но этот файл почти ничего не делает. Файл Windowl.xaml, создаваемый Visual Studio, по умолчанию определяет элемент Window и Grid. Windowl.xaml <W1 ndow x:Class="Gradi entBrushResourceDemo.WIndowl" xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml" T1tle="Grad1entBrushResourceDemo" He1ght="300" W1dth="300" <Gr1d> </Gr1d> </WIndow> Исходная версия файла программной логики Windowl.xaml.cs просто содер- жит вызов InitializeComponent. Windowl.xaml.es using System; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; namespace GradientBrushResourceDemo /// <summary> /// Логика взаимодействия для Windowl.xaml /// </summary> public partial class Windowl ; Window { public WIndowl() { InitializeComponentO; } В этот код включается одна команда С#, определяющая ресурс окна.
552 Глава 21. Ресурсы Windowl.xaml.cs using System; using System.Windows: using System.Windows.Controls: using System.Windows.Data; using System.Windows.Documents: using System.Windows.Media; using System.Windows.Media.Imaging: using System.Windows.Shapes: namespace GradientBrushResourceDemo { /// <summary> /// Логика взаимодействия для Windowl.xaml /// </summary> public partial class Windowl : Window { public Windowl0 { Resources.Add("thicknessMargin". new Thickness(24. 12, 24, 23)); InitializeComponent(); } } В файле Windowl.xaml объект StackPanel заменяет Grid, а четыре элемента Text- Block могут использовать ресурсы LinearGradientBrush и Thickness. Windowl.xaml <Window x:Class="GradientBrushResourceDemo.Wi ndowl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Title="GradientBrushResourceDemo" Height="300" Width="300" > <StackPanel> <TextBlock Margin="{StaticResource thicknessMargin}" Foreground="{StaticResource brushGradient}"> Gradient text </TextBlock> <TextBlock Margin="{StaticResource thicknessMargin}" Foreground="{StaticResource brushGradient}"> Of black, green, and gold </TextBlock> <TextBlock Margin="{StaticResource thicknessMargin}" Foreground="{StaticResource brushGradient}"> Makes an app pretty, </TextBlock>
553 ресурсы <TextBlock Margin="{StaticResource thicknessMargin}" Foreground="{StaticResource brushGradient}"> Makes an app bold. </TextBlock> </StackPanel> </Window> He скрою, я не люблю пользоваться автоматически сгенерированными проек- тами Visual Studio для создания примеров. Одна из проблем заключается в том, что мне приходится переименовывать все файлы подряд, чтобы они имели более осмысленные названия, чем MyApp и Windowl. Но теперь, когда вы знаете, что оз- начает тег Application.Resource и как им пользоваться, ничто не мешает вам поль- зоваться проектам Visual Studio и дизайнером XAML (только мне не говорите). В начале главы я описал возможный способ определения двух размеров шриф- тов в статических полях, доступных только для чтения. Оказывается, в XAML существует расширение разметки x:Static, предназначенное специально для об- ращения к статическим свойствам или полям. Оно также работает с членами пе- речислений. Предположим, вы хотите задать свойству Content объекта Button статическое свойство с именем SomeStaticProp класса SomeClass. Синтаксис расширения раз- метки выглядит так: Content="{х:Static SomeClass:SomeStaticProp}" Также элемент x:Static может использоваться с синтаксисом элемента свойства: <Button.Content> <x:Static Member="SomeClass:SomeStaticProp" /> </Button.Content> Тип статического поля или свойства должен соответствовать типу задаваемо- го свойства, или должен существовать конвертор для проведения преобразова- ния (конечно, свойство Content относится к типу object, поэтому подойдет любой тип). Например, если вы хотите задать некоторому элементу одинаковую высо- ту с полосой заголовка, используйте следующую команду: Height="{x:Static SystemParameters.CaptionHeight}" Выбор не ограничивается статическими свойствами или полями, определен- ными в WPF, но при обращении к классам, не входящим в WPF, потребуется объявление пространства имен XML для пространства CLR, в котором находят- ся классы. Следующая автономная программа XAML связывает префикс XML s с про- странством имен System для вывода информации из статических свойств в клас- се Environment. Environmentlnfo.xaml <! - - ================================================== Environmentlnfo.xaml (с) 2006 by Charles Petzold
554 Глава 21. Ресурсы <StackPanel xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:s="clr-namespace:System;assembly=mscorli b"> <TextBlock> <Label Content="Operating System Version: " /> <Label Content="{x:Static s:Environment.OSVersion}" /> <LineBreak /> <Label Content=".NET Version: " /> <Label Content="{x:Static s:Environment.Version}" /> <LineBreak /> <Label Content="Machine Name: " /> <Label Content="{x:Static s:Environment.MachineName}" /> <LineBreak /> <Label Content="User Name: " /> <Label Content="{x:Static s:Environment.UserName}" /> <LineBreak /> <Label Content="User Domain Name: " /> <Label Content="{x:Static s:Environment.UserDomainName}" /> <LineBreak /> <Label Content="System Directory: " /> <Label Content="{x:Static s:Environment.SystemDirectory}" /> <LineBreak /> <Label Content="Current Directory: " /> <Label Content="{x:Static s:Environment.CurrentDirectory}" /> <LineBreak /> <Label Content="Command Line: " /> <Label Content="{x:Static s:Environment.CommandLine}" /> </TextBlock> </StackPanel> Программа выводит только текстовое представление свойств. Большая часть свойство возвращает строковые данные, но два свойства являются исключени- ем. Свойство OSVersion (текущая версия Microsoft Windows) относится к типу OperatingSystem, а свойство Version (версия .NET) — к типу Version. К счастью, ме- тоды ToString этих классов форматируют данные к удобочитаемому виду. Этим двум расширениям x:Static нельзя просто задать свойство Text объекта TextBlock (к примеру). Не существует автоматического преобразования этих не- строковых объектов в строку. Вместо этого я задаю выражения x:Static свой- ствам Content элементов Label. Свойству Content, как вы помните, можно задать любой объект, и представление последнего будет сформировано его методом ToString. Назначение элементов Label дочерними по отношению к TextBlock (при этом они фактически становятся частью элементов InlineUIContainer) позволяет использовать элементы LineBreak для разбиения вывода на строки. Этот файл можно запустить в XAML Cruncher или аналогичной програм- ме, но он не будет работать в Internet Explorer. Для всех свойств, кроме OSVersion и Version, программе необходимы разрешения безопасности, недопустимые при запуске XAML в Internet Explorer.
ресурсы 555 Другая методика использования x:Static основана на определении статиче- ских полей или свойств в исходном коде C# и последующем обращении к ним из XAML-файлов проекта. Следующий проект называется AccessStaticFields. Он содержит XAML-файл и файл с кодохМ С#, образующие уже знакомый класс Window, но в проект также входит файл C# с именем Constants.cs; определяемый в нем класс Constants содержит тройку статических полей и свойств, доступных только для чтения. Таким образом, класс является полностью статическим. Constants.cs //------------------------------------------ // Constants.cs (с) 2006 by Charles Petzold и------------------------------------------- using System; using System.Windows; using System.Windows.Media; namespace Petzold.AccessStaticFiel ds { public static class Constants { // Открытые статические члены public static readonly FontFamily fntfam = new FontFamily("Times New Roman Italic"); public static double FontSize { get { return 72/0.75: } } public static readonly LinearGradientBrush brush = new LinearGradientBrush(Colors.LightGray. Colors.DarkGray, new Point(0, 0). new Pointd, D); } } Я определил два информационных объекта как статические поля, доступные только для чтения, и один — как статическое свойство. Это сделано исключи- тельно ради разнообразия; в данном примере это несущественно. (Более того, ограничение доступа не обязательно, но эти поля не могут задаваться из XAML- файла, поэтому любое другое определение все равно не добавит функциональ- ности.) Поскольку эти статические поля и свойства определяются в вашем ис- ходном коде, а не в стандартных сборках WPF, XAML-файл должен содержать объявление пространства имен XML для определения префикса, ассоциируемо- го с пространством имен класса, в котором находятся поля или свойства. Следующий XAML-файл ассоциирует префикс XML src с пространством имен CLR Petzold.AccessStaticFields. Затем файл использует расширение разметки
556 Глава 21. Ресурсы x:Static для обращения к статическим полям и свойствам. Два расширения раз- метки используют синтаксис атрибутов, а третье — синтаксис элемента свойства (снова исключительно для разнообразия). AccessStaticFields.xaml <।_ _ ===================================================== AccessStaticFields.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xmlns:src="clr-namespace:Petzold.AccessStaticFields" x:Class="Petzold.AccessStaticFiel ds.AccessStaticFiel ds" Title="Access Static Fields" SizeToContent="WidthAndHeight"> <TextBlock Background^'{x:Static src:Constants.brush}" FontSize="{x:Static src:Constants.FontSize}" TextAlignment="Center"> <TextBlock.FontFamily> <x:Static Member="src:Constants.fntfam" /> </TextBlock.FontFamily> Properties from<LineBreak />Static Fields </TextBlock> </Window> Файл программной логики тривиален. AccessStaticFields.es //---------------------------------------------------- // AccessStaticFields.es (с) 2006 by Charles Petzold //---------------------------------------------------- using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input; using System.Windows.Media: namespace Petzold.AccessStati cFi el ds { public partial class AccessStaticFields : Window { [STAThread] public static void MainO { Application app = new ApplicationO: app.Run(new AccessStat!cFiel ds()): public AccessStaticFields() {
ресурсы 557 Initial!zeComponent(); } } } Конечно, статические поля и свойства не обязаны находиться в отдельном файле. В ранней версии проекта я разместил их прямо в коде C# класса Access- StaticFields, а расширения х: Static ссылались на класс src:AccessStaticFields вместо src:Constants. Но мне понравилась сама идея — выделить целый класс, состоя- щий исключительно из статических членов, под константы, используемые при- ложением. Теперь вы знаете, как определять объекты в качестве ресурсов и ссылаться на них в расширении разметки StaticResource. Также вы умеете обращаться к ста- тическим свойствам и полям классов в синтаксисе x:Static. Остается разобраться с возможностью ссылаться на свойства экземпляров и поля конкретного объ- екта. Для этого необходимо указать как объект, так и свойство этого объекта, а синтаксис выходит далеко за рамки возможностей StaticResource и x:Static. В гла- ве 23 будет показано, как это делается. А вот еще один пример использования x:Static. Автономный XAML-файл зада- ет атрибуты Content и Foreground элемента Label при помощи выражений x:Static. DisplayCurrentDateTime.xaml < । _ ========================================================= DisplayCurrentDateTime.xaml (с) 2006 by Charles Petzold <Label xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:s="clr-namespace:System;assembly=mscorli b" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="48" Content="{x:Stati c s:DateTime.Now}" Foreground="{x:Static SystemColors.ActiveCaptionBrush}" /> Объект DateTime.Now относится к типу DateTime, а свойство Content выводит строку, возвращаемую методом ToString структуры, поэтому результат нормаль- но отформатирован. Последняя строка XAML-файла задает свойству Foreground объект Brush из статического свойства SystemColors.ActiveCaptionBrush, поэтому текст выводится тем же цветом, что и текст в полосе заголовка окна. Надеюсь, вы не ждете, что время будет автоматически обновляться каждую се- кунду? Если для запуска DisplayCurrentDateTime.xaml используется XAML Cruncher (или другая аналогичная программа), обращение к свойству DateTime.Now про- исходит всего один раз — при создании элемента Label. Если ввести в XAML- файле обычный пробел или нажать F6, XAML Cruncher передаст «новую» вер- сию XamIReader.Load, и время обновится. Как вы думаете, что произойдет при изменении системных цветов? Будет ли программа автоматически обновлять цвет текста Label? Попробуйте! Вызовите
558 Глава 21. Ресурсы окно свойств экрана, перейдите на вкладку Оформление и выберите в списке другую цветовую схему. Щелкните на кнопке ОК. Вы увидите, что заголовки всех программ, работающих в Windows, изменяют цвет. Заголовок XAML Cruncher тоже меняет цвет, но цвет текста в элементе Label остается прежним. Ничего удивительного — та же ситуация, что и с датой/временем. Обраще- ние к статическому свойству SystemColors.ActiveCaptionBrush происходит только при создании Label; не существует автоматического механизма обновления эле- мента при изменении свойства. Конечно, как и в случае с датой/временем, при вводе пробела или нажатии F6 файл DisplayCurrentDateTime.xaml разбирается за- ново, а элемент Label создается с новым системным цветом. Хотите, чтобы цвет текста Label автоматически обновлялся при изменении системных цветов? Это в ваших силах. Классы SystemColors, SystemParameters и SystemFonts содержат огромные кол- лекции статических свойств, к которым XAML-файл может обращаться с исполь- зованием расширения разметки x:Static. Присмотревшись к этим трем классам, можно заметить одну странность: все статические свойства существуют парами. Для каждого свойства с именем Whatever также существует свойство Whatever®/. Все свойства с окончанием Key возвращают объекты типа ResourceKey. Статическое свойство SystemColors.ActiveCaptionBrush возвращает объект типа SolidColorBrush, поэтому абсолютно логично, что этот объект может быть задан свойству Foreground объекта Label, для которого требуется объект типа Brush. Свойство SystemColors.ActiveCaptionBrushKey возвращает объект типа ResourceKey. Этот объект ResourceKey также должен предоставить средства для обращения к кисти, полученной из SystemColors.ActionCaptionBrush. ResourceKey содержит ключ словаря (ассоциативного списка) — аналог ключей, определяемых в ресурсах XAML с атрибутом х:Кеу. Объект ResourceKey отличается от них тем, что он со- держит свойство типа Assembly, чтобы парсер XAML знал, в какой сборке хра- нится словарь. Казалось бы, к кисти можно обратиться при помощи расширения разметки StaticResource с ключом, возвращаемым SystemColors.ActiveCaptionBrushKey. Напри- мер, можно предложить такую конструкцию: Foreground^'{StaticResource SystemColors.ActiveCaptionBrushKey}" Однако такой способ не работает. Попытавшись подставить эту строку на ме- сто атрибута Foreground в файле DisplayCurrentDateTime.xaml, вы получите сообще- ние об ошибке (ресурс не найден). И это вполне логично — парсер ищет ресурс с текстовым ключом «SystemColors.ActiveCaptionBrushKey». Нам же нужен ре- сурс, на который ссылается ключ, возвращаемый статическим свойством System- Colors.ActiveCaptionBrushKey. Проблема проще, чем может показаться. Следующее расширение разметки возвращает объект типа SolidColorBrush: {/X:Static SystemColors.ActiveCaptionBrush} Следовательно, это расширение должно вернуть объект типа ResourceKey: {х:Static SystemColors.ActiveCaptionBrushKey}
559 pecypcbl Объект типа ResourceKey — именно то, что нужно StaticResource. Попробуем вложить выражение x:Static в выражение StaticResource: Foreground="{StaticResource {х:Static SystemColors.ActiveCaptlonBrushKey}}" Работает! Обратите внимание: одна пара фигурных скобок вложена в дру- гую, так что все выражение завершается двумя правыми фигурными скобками. Новое значение Foreground функционально эквивалентно исходному: Foreground="{х:Static SystemColors.Acti veCapti onBrush}" Тем не менее альтернативный синтаксис вроде бы не дает особых преиму- ществ. Если вы попытаетесь запустить новый XAML-файл и изменить систем- ные цвета, цвет текста Label автоматически меняться не будет. Давайте внесем еще одно изменение в альтернативный синтаксис — заменим StaticResource на DynamicResource. Это третье и последнее расширение разметки, которое я представлю в этой главе: Foreground="{DynamicResource {х:Static SystemColors.ActiveCaptlonBrushKey}}" Мы добились жслаехмого эффекта! Теперь при изменении системной цвето- вой схемы цвет текста Label меняется вместе с цветами заголовков. Вероятно, для полноты картины стоит показать синтаксис элементов свойств для DynamicResource. В файле DisplayCurrentDateTime.xaml необходимо добавить конечный тег для Label, чтобы свойство Foreground могло быть оформлено как элемент свойства: <Label ... > <Label.Foreground> <DynamicResource> <Dynami cResource.ResourceKey> <x:Static Member="SystemColors.ActiveCaptionBrushKey" /> </DynamicResource.ResourceKey> </DynamicResource> </Label.Foreground> </Label> StaticResource и DynamicResource представляют два разных подхода к обраще- нию к ресурсам. В обоих случаях необходимы ключи, используемые для обра- щения к объектам. Но StaticResource обращается к объекту по ключу и сохраняет полученный объект. DynamicResource сохраняет ключ и обращается к объекту по мере необходимости. При изменении системных цветов Windows рассылает сообщение системно- го уровня. Как правило, приложения реагируют на него, объявляя недействи- тельным изображения своих окон. В WPF эта операция преобразуется в вызов InvalidateVisual, означающий, что для каждого элемента будет вызван метод On- Render. Если (например) свойство Foreground элемента ссылается на динамиче- ский ресурс, сохраненный ключ используется для обращения к кисти. Впро- чем, это вовсе не означает, что весь элемент создается заново. При изменении системных цветов и цвета текста Label содержимое Label остается прежним: дата и время, выводимые элементом, не обновляются.
560 Глава 21. Ресурсы Основной областью применения DynamicResource является обращение к сис- темным ресурсам — таким, как системные цвета. Не ждите от DynamicResource слишком многого — концепции оповещения об изменении ресурсов не сущест- вует. Чтобы элементы управления и интерфейсные элементы обновлялись в со- ответствии с изменением свойств других объектов, используйте привязку дан- ных (см. главу 23). Обычно опережающие ссылки на ресурсы невозможны. Иначе говоря, ресурс, на который ссылается расширение разметки StaticResource, уже должен быть оп- ределен в файле или элементе-предке. Однако в некоторых ситуациях было бы удобно ссылаться на ресурс до его определения. Например, начальный тег па- нели мог бы содержать атрибут Background со ссылкой на ресурс, определяемый в секции Resources: <StackPanel Background^'{StaticResource mybrush}"> <StackPanel.Resources> <SolidColorBrush x:Key=,,mybrush" ... /> </StackPanel.Resources> Такое решение не работает. Можно убрать атрибут Background из начального тега и воспользоваться синтаксисом элемента свойства: <StackPanel> <StackPanel.Resources> <Sol1dColorBrush x:Key="mybrush" ... /> </StackPanel.Resources> <StackPanel.Background> <StaticResource ResourceKey="mybrush" /> </StackPanel.Background> А можно заменить StaticResource на DynamicResource — в этом случае обраще- ние к ресурсу откладывается до момента, когда ресурс понадобится для отобра- жения панели. Кисти, которые вы создаете как ресурсы, сами могут базироваться на сис- темных цветах. В следующем автономном XAML-файле две кисти определяют- ся в виде ресурсов. Кисть LinearGradientBrush определяет градиент между актив- ным и неактивным цветом заголовка. Вторая кисть SolidColorBrush использует расширение DynamicResource с SystemColors.ActiveCaptionColorKey. DynamicResourceDemo.xaml <I_ _ ====================================================== DynamicResourceDemo.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Background^'{DynamicResource
561 ресуРсЫ {х:Static SystemColors.InactlveCaptionBrushKey}}"> <StackPanel,Resources> <L1nearGrad1entBrush x:Key=,,dynabrushr' StartPo1nt="0 0" EndPo1nt="l 1"> <L1nearGradientBrush.GradlentStops> <Grad1entStop 0ffset="0” Color="{DynamlcResource (x:Stat1c SystemColors.ActiveCaptionColorKey}}" /> <Grad1 entStop Offset=,,l" Color="{DynamlcResource {x:Stat1c SystemColors.InactlveCaptionColorKey}}" /> </L1nearGradlentBrush.GradlentStops> </L1nearGrad1entBrush> <SolIdColorBrush x:Key="dynabrush2" Color="{DynamlcResource {x:Stat1c SystemColors.ActiveCaptlonColorKey}}" /> </StackPanel.Resources> <Label HorizontalAl 1gnment="Center" FontSize="96" Content="Dynam1c Resources" Background^'{StaticResource dynabrushl}" Foreground="{Stat1cResource dynabrush2}" /> </StackPanel> Два ресурса используют DynamicResource для обращения к SystemColors.Active- CaptionColorKey и SystemColors.InactiveCaptionColorKey. Речь идет о ключах, ссылаю- щихся на цвета, а не на кисти, потому что они используются для задания свой- ства Color двух объектов Gradientstop. В программе используются три варианта цветового оформления. В верхней части фон StackPanel определяется расширением DynamicResource, базирующимся на SystemColors.InactiveCaptionBrushKey. Цвета текста и фона нижнего объекта Label задаются двумя локально определяемыми ресурсами, однако в данном слу- чае две ресурсные кисти определяются как статические ресурсы. При изменении системных цветов две кисти, определяемые как локальные ресурсы, тоже изменя- ются с получением новых свойств Color. Однако сами объекты LinearGradientBrush и SolidColorBrush при этом не заменяются новыми объектами. Элемент Label свя- зан с этими двумя объектами, поэтому при изменении объектов цвета текста и фона Label отражают изменение системных цветов. Если изменить атрибуты Background и Foreground элемента Label на Dynamic- Resource, программа перестает реагировать на изменения системных цветов! Проблема в том, что DynamicResource ожидает, что определяемый ключом объ- ект будет создан заново. Объекты кистей заново не создаются, поэтому Dynamic- Resource не обновляет свойства Foreground и Background (по крайней мере это лучшее объяснение, которое я могу предложить).
562 Глава 21. Ресурсы Ключи, ассоциированные с системными цветами и другими системными дан- ными, могут использоваться в ваших определениях ресурсов. В этом случае сис- темные определения ресурсов заменяются локальными. Следующий автономный XAML-файл является разновидностью программы ResourceLookupDemo.xaml, при- веденной ранее в этой главе. AnotherResourceLookupDemo.xaml <।_ _ ============================================================ AnotherResourceLookupDemo.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Orientation="Horizontal "> <StackPanel> <StackPanel.Resources> <Soli dColorBrush x:Key="{x:Static SystemColors.ActiveCaptlonBrushKey}" Color="Red" /> </StackPanel.Resources> <Button Horizontal Al ignment=,,Center" VerticalAlignment="Center" Margin=,,24" Foreground^' {DynanricResource {x:Static SystemColors.ActiveCaptionBrushKey}}"> Button with Red text </Button> </StackPanel> <StackPanel> <Button Horizontal Alignment="Center" VerticalAlignment="Center" Margin="24" Foreground^' {DynamicResource {x:Static SystemColors.ActiveCaptionBrushKey}}"> Button with Blue text </Button> </StackPanel> </StackPanel> Только первый вложенный элемент StackPanel содержит секцию Resources, в которой определяется красная кисть с ключом, полученным из SystemColors. ActiveCaptlonBrushKey. Объекту Button в этом элементе StackPanel назначается крас- ная кисть, а другому объекту Button назначается кисть цвета активного заголов- ка, которая меняется с изменением системных цветов. Вероятно, когда у вас появится некоторый опыт работы с ресурсами, вам захочется организовать общий доступ к ресурсам в нескольких приложениях.
ресурсы 563 Например, такая необходимость может возникнуть при разработке коллекции пользовательских стилей, которые придают приложениям вашей компании их неповторимое оформление. Ресурсы, которые должны совместно использоваться в нескольких проектах, могут объединяться в XAML-файлах с корневым элементом ResourceDictionary. Каждый ресурс представляется дочерним элементом этого корневого элемента. Приведенный далее словарь содержит всего один ресурс (хотя ресурсов может быть и много). MyResourcesl.xaml <! - - =============================================== MyResourcesl.xaml (с) 2006 by Charles Petzold ResourceDictionary xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml /presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <LinearGradientBrush x:Key=,,brushLinear"> <LinearGradlentBrush.Gradi entStops> <GradientStop Color="Pink" Offset="0" /> <GradientStop Color="Aqua" Offset='T' /> </L inearGradlentBrush.Gradi entStops> </L inearGradlentBrush> </ResourceDictionary> А вот другой пример словаря, который тоже может содержать много ресур- сов, но в данном примере ограничивается всего одним. MyResourcesZ.xaml <1 __ =============================================== MyResources2.xaml (с) 2006 by Charles Petzold ResourceDictionary xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> RadialGradientBrush x:Key="brushRadial"> <Radi alGradi entBrush.Gradi entStops> <GradientStop Color="Pink" Offset=,,0" /> <GradientStop Color="Aqua" Offset=,T' /> </RadialGradi entBrush.Gradi entStops> </Radi alGradi entBrush> </ResourceDi cti ona ry> Мы создаем проект UseCommonResources и хотим использовать в нем ресур- сы, определяемые в файлах MyResourcesl.xaml и MyResources2.xaml. Эти два файла становятся частью проекта. Свойство Build Action для этих файлов может быть равно Раде или Resource (лучше Раде, потому что на стадии компиляции выпол- няется предварительная обработка, в результате которой файл преобразуется из XAML в BAML). Файл определения приложения для этого проекта может со- держать секцию Resources, приведенную в следующем файле.
564 Глава 21. Ресурсы UseCommonResourcesApp.xaml <i__ ======================================================== UseCommonResourcesApp.xaml (с) 2006 by Charles Petzold Application xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" StartupUri="UseCommonResourcesWindow.xaml"> <App1i cati on.Resources> <ResourceDi cti onary> <ResourceDi cti onary.MergedDi cti onari es> <ResourceDictionary Source="MyResourcesl.xamr /> <ResourceDictionary Source="MyResources2.xaml" /> </ResourceDi cti onary.MergedDi cti onari es> </ResourceDi cti onary> </App1i cati on.Resources> </Application> В секции Resources файла находится элемент ResourceDictionary. Он определяет свойство MergedDictionaries — коллекцию других объектов ResourceDictionary, зада- ваемых по именам файлов. Если в проекте используется только один словарь ресурсов, для работы с ними можно использовать один объект ResourceDictionary без элемента свойства ResourceDictionary.MergedDictionaries. Словари объединяются слиянием. Если некоторый ключ присутствует в не- скольких файлах, в процессе слияния более ранний ресурс с этим ключом заме- няется более поздним ресурсом. Вместо файла определения приложения элемент ResourceDictionary может на- ходиться в секции Resources XAML-файла, но ресурсы будут доступны только в этом файле, а не на уровне всего приложения. Наконец, следующий элемент Window использует ресурсы, определяемые в фай- лах MyResourcesl.xaml и MyResources2.xaml. UseCommonResourcesWindow.xaml <।_ _ =========================================================== UseCommonResourcesWindow.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Title="Use Common Resources" Background^'{StaticResource brushLinear}"> <Button FontSize="96pt" Horizontal Alignment^’Center" VerticalAlignment="Center" Background^'{StaticResource brushRadial}"> Button </Button> </Window>
Окна, страницы X “ перемещение В оставшейся части книги исследуются многие возможности и функции XAML; в основном для этой цели используются небольшие автономные XAML-файлы. Конечно, они демонстрируют важные аспекты работы с XAML, но если сосредо- точиться исключительно на мелких файлах, легко потерять из виду общую кар- тину. По этой причине я хочу представить в этой главе полноценную програм- му с меню и диалоговыми окнами, объединяющую код XAML и С#. Также мне хотелось подчеркнуть контраст с классическими навигационными приложениями WPF. Приложение WPF (или его часть) можно структурировать так, чтобы оно работало как серия взаимосвязанных страниц веб-сайта. Вместо использования одного окна, принимающего пользовательский ввод и команды от меню и диалоговых окон, навигационные приложения часто изменяют содер- жимое своего окна (или его частей) при помощи гиперссылок. Такие приложе- ния по-прежнему остаются клиентскими, но по своему поведению имеют много общего с веб-приложениями. В этой главе также рассматриваются три формата файлов, используемых при распространении приложений WPF. Конечно, первое место занимает тради- ционный формат .ехе — мы уже видели много WPF-приложений, для которых создаются .ехе-файлы. Его противоположностью является автономный XAML- файл, созданный в XAML Cruncher (или другой аналогичной программе) и вы- полняемый в Microsoft Internet Explorer. Промежуточное место между этими крайностями занимает формат .xbap (XAML Browser Application). Как подсказывает само название, приложения .xbap, как и автономные XAML-файлы, работают под управлением Internet Explorer. Однако они обычно содержат как код XAML, так и код C# и обрабатываются компилятором. Так как эти приложения должны работать в контексте Internet Explorer, их функциональность ограничивается по соображениям безопасности. Короче говоря, такие приложения не могут сделать ничего, что могло бы повре- дить компьютеру пользователя, поэтому приложения .xbap могут запускаться без подтверждения. Начнем с приложения, имеющего традиционную структуру и распространяемо- го в формате .ехе. Программа, о которой пойдет речь, строится на базе элемента InkCanvas, предназначенного для получения и отображения ввода со стилуса на планшетных компьютерах. Класс InkCanvas также реагирует на ввод с мыши как
566 Глава 22. Окна, страницы и перемещение на планшетных, так и настольных компюьтерах. Чтобы убедиться в этом, доста- точно запустить крошечный автономный XAML-файл. JustAnlnkCanvas.xaml <1__ ================================================== JustAnlnkCanvas.xaml (с) 2006 by Charles Petzold <InkCanvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" /> Сначала я хотел изобразить разлинованный блокнот, на страницах которого мог бы рисовать пользователь. Когда стало ясно, что для сохранения многих страниц потребуется специальный формат файла, я решил ограничиться одной страницей. Файл YellowPadWindow.xaml определяет макет главного окна приложения. Ос- новная часть кода XAML посвящена определению меню программы. Каждая ко- манда меню представлена элементом типа Menuitem; команды образуют иерар- хию с корневым элементом Menu. Объект Menu пристыкован у верхнего края DockPanel. Обратите внимание: у некоторых команд меню свойствам Command за- даются различные статические свойства класса Applicationcommands — например, New, Open и Save. У других команд устанавливаются обработчики событий Click. YellowPadWindow.xaml <i _ ================================================== YellowPadWindow.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:src="clr-namespace:Petzold.Yel1owPad" x:Class="Petzold.Yel1owPad.Yel1owPadWi ndow" Title="Yellow Pad- Si zeToContent="Wi dthAndHei ght"> <DockPanel> <Menu DockPanel.Dock="Top"> <!-- Меню File --> <Menultem Header="_File"> <MenuItem Header="_New" Command="New" /> <MenuItem Header="_Open..." Command="Open" /> <MenuItem Header="_Save..." Command=,,Save" /> Separator /> <MenuItem Header="E_xit" Command^’Close" /> </MenuItem> <!-- Меню Edit --> <MenuItem Header="_Edit" SubmenuOpened="EditOnOpened"> <MenuItem Header="Cu_t" Command="Cut" /> <MenuItem Header="_Copy" Command="Copy" />
Окна, страницы и перемещение 567 <MenuItem Header="_Paste" Command="Paste" /> <MenuItem Header="_Delete" Command="Delete" /> Separator /> <MenuItem Header="Select _A11" Command="SelectAl1" /> <MenuItem Header="_Format Selection..." Name="itemForniat" Cl 1ck="FormatOnCli ck"/> </MenuItem> <!-- Меню Stylus-Mode --> <MenuItem Header=,,_Stylus-Mode" SubnienuOpened=,,StylusModeOnOpened"> <MenuItem Header=,,_Ink" Click="StylusModeOnClick" Tag="{x:Static InkCanvasEditingMode.Ink}" /> <MenuItem Header="Erase by _Point" Click="StylusModeOnClick" Tag="{x:Static InkCanvasEditingMode.EraseByPoint}" /> <MenuItem Header="_Erase by Stroke" Click="StylusModeOnClick" Tag="{x:Static InkCanvasEditingMode.EraseByStroke}" /> <MenuItem Header="_Select" Click="StylusModeOnClick" Tag="{x:Static InkCanvasEditingMode.Select}" /> </MenuItem> <!-- Меню Eraser-Mode (только на планшетных компьютерах) --> <MenuItem Header="E_raser-Mode" SubmenuOpened="EraserModeOnOpened" Name="menuEraserMode"> <MenuItem Header="_Ink" Click="EraserModeOnClick" Tag="{x:Static InkCanvasEditingMode.Ink}" /> <MenuItem Header="Erase by _Point" Cli ck="EraserModeOnCli ck" Tag="{x:Static InkCanvasEditingMode.EraseByPoint}" /> <MenuItem Header="_Erase by Stroke" Cli ck="EraserModeOnCli ck" Tag="{x:Static InkCanvasEditingMode.EraseByStroke}" /> <MenuItem Header="_Select" Click="EraserModeOnClick" Tag="{x:Static InkCanvasEditingMode.Select}" /> </MenuItem> <!-- Меню Tools --> <MenuItem Header="_Tools"> <MenuItem Header="_Stylus..." Click="StylusToolOnClick" /> <MenuItem Header="_Eraser..." Click="EraserTocjlOnClick"/> </MenuItem> <!-- Меню Help --> <MenuItem Header="_Help"> <MenuItem Header="_Help..." Command="Help" /> <MenuItem Header="_About YellowPad..."
568 Глава 22. Окна, страницы и перемещение Cl 1ck="About0nC11ck,7> </MenuItem> </Menu> <!-- Scrollviewer содержит элемент InkCanvas --> <Scrol1VIewer VerticalScrollBarV1s1b111ty="Auto" > <InkCanvas Name="1nkcanv" W1dth="{x:Static src:YellowPadWIndow.widthCanvas}" Helght=" {x:Static src:Yel1owPadWIndow.helghtCanvas}" Background="LemonCh1ffon”> <L1ne Stroke="Red" Xl="0.8751n" Yl="0" X2=”0.8751 n” Y2="{x:Stat1c src:YellowPadWIndow.helghtCanvas}" /> <L1ne Stroke="Red" Xl="0.93751n" Yl="0" X2="0.93751n" Y2="{x:Static src:YellowPadWIndow.helghtCanvas}" /> </InkCanvas> </ScrollV1ewer> </DockPanel> <!-- Объединение всех объектов CommandBinding --> <W1ndow.CommandBindi ngs> <CommandB1nd1ng Command="New" Executed="NewOnExecuted" /> <CommandB1nd1ng Command="Open" Executed="OpenOnExecuted" /> <CommandB1nd1ng Command="Save" Executed="SaveOnExecuted" /> <CommandB1nd1ng Command="Close" Executed="CloseOnExecuted" /> <CommandB1nd1ng Command="Cut" CanExecute="CutCanExecute" Executed="CutOnExecuted" /> <CommandB1nd1ng Command="Copy" CanExecute="CutCanExecute" Executed="CopyOnExecuted" /> <CommandB1nd1ng Command="Paste" CanExecute="PasteCanExecute" Executed="PasteOnExecuted" /> <CommandB1nd1ng Command="Delete" CanExecute="CutCanExecute" Executed="DeleteOnExecuted" /> <CommandB1nd1ng Command="SelectAll" Executed="SelectA110nExecuted" /> <CommandB1nd1ng Command="Help" Executed="HelpOnExecuted" /> </WIndow.CommandBindings> </W1ndow> После определения меню XAML-файл заполняет DockPanel объектом Scroll- Viewer с внутренним объектом InkCanvas. Объект InkCanvas закрашивается фоно- вым цветом LemonChiffon, и в нем рисуются две вертикальные красные линии. Размеры InkCanvas берутся из статических членов класса YellowPadWindow (они определяются в файле С#, приведенном далее). XAML-файл завершается всеми элементами CommandBinding, необходимыми для привязки свойств Command ко- манд меню к обработчикам CanExecute и OnExecuted. Я разделил код C# класса YellowPadWindow на шесть небольших файлов. Пер- вый файл называется YellowPadWindow.cs, а остальные — по названиям меню верх- него уровня (например, YellowPadWindow.File.cs).
ОКна, страницы и перемещение 569 файл YellowPadWindow.cs начинается с определения желательных размеров InkCanvas в виде открытых статических полей, доступных только для чтения. К этим размерам обращается файл YellowPadWindow.xaml. YellowPadWindow.cs И-- // YellowPadWindow.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.YellowPad { public partial class YellowPadWindow : Window { // Панель размером 5 на 7 дюймов public static readonly double widthCanvas = 5 * 96; public static readonly double heightCanvas = 7 * 96; [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new YellowPadWindowO); } public YellowPadWindowO InitializeComponent(); // Рисование синих горизонтальных линий на расстоянии 1/4 дюйма double у = 96; while (у < heightCanvas) Line line = new LineO; line.XI = 0; line.Yl = y; line.X2 = widthCanvas; line.Y2 = y; line.Stroke = Brushes.LightBlue; i nkcanv.Chi 1dren.Add(line); у += 24; }
570 Глава 22. Окна, страницы и перемещение // Отключение меню Eraser-Mode. // если компьютер не является планшетным If (Tablet.TabletDevices.Count == 0) menuEraserMode.VlsIblllty = Visibility.Collapsed; } } Конечно, конструктор YellowPadWindow вызывает InitializeComponent, но он так- же отвечает за рисование синих горизонтальных линий на расстоянии в чет- верть дюйма. Сначала я хотел поместить элементы Line в XAML-файл, но мне не хотелось создавать столько повторяющихся элементов разметки. Кроме того, эти элементы пришлось бы изменять в случае изменения вертикальных разме- ров «холста». Напоследок конструктор отключает одно из меню верхнего уровня, если про- грамма выполняется не на планшетном компьютере. На настольных компьюте- рах удаление этого меню не ограничивает функциональность программы. По мере того как пользователь рисует на поверхности InkCanvas стилусом или мышью, InkCanvas сохраняет введенные данные в свойстве Strokes типа Stroke- Collection — коллекции объектов Stroke (хотя InkCanvas определяется в простран- стве имен System.Windows.Controls, Stroke и Strokecollection определяются в System. Windows.Ink). На планшетных компьютерах штрихом называется операция, при которой пользователь прикасается стилусом к экрану, перемещает и отводит его от экрана. При работе с мышью штрих выполняется иначе — пользователь нажи- мает левую кнопку мыши, перемещает мышь и отпускает левую кнопку. В обо- их случаях InkCanvas отслеживает перемещение стилуса или мыши и рисует ли- нию на экране. В компьютерной графике штрих фактически представляет собой ломаную — совокупность коротких соединенных отрезков, определяемых в виде серии то- чек. Соответственно, объект Stroke содержит свойство StylusPoints типа StylusPoint- Collection — коллекции объектов StylusPoint (классы StylusPoint и StylusPointCollection определяются в пространстве имен System.Windows.Input). Структура StylusPoint содержит свойства X и Y, обозначающие координаты точки, а также свойство PressureFactor, в котором хранится сила нажатия стилу- са. По умолчанию InkCanvas при сильном нажатии рисует более широкие линии. (Конечно, при работе с мышью толщина линий остается постоянной.) Возможно, в будущем планшетные компьютеры будут сохранять и другую информацию, кроме позиции и силы нажатия; для работы с этой информацией в структуре StylusPoint определяется свойство Description. Кроме свойства StylusPoints, класс Stroke также определяет свойство Drawing- Attributes. Теоретически каждый штрих может рисоваться отдельным цветом; этот цвет является частью объекта DrawingAttributes. DrawingAttributes также содержит свойства Width и Height, определяющие ширину и высоту линии, нарисованной стилусом или мышью. Изменяя их, можно создавать причудливые каллиграфиче- ские эффекты. Наконечник стилуса может иметь прямоугольную или эллип- тическую форму и даже может поворачиваться. InkCanvas содержит свойство
Окна, страницы и перемещение 571 DefaultDrawingAttributes с объектом DrawingAttributes; этот набор атрибутов приме- няется к текущему и ко всем будущим штрихам, пока свойство не изменится. Strokecollection определяет метод Save, который сохраняет коллекцию штри- хов в формате ISF (Ink Serialized Format), а также конструктор для загрузки ISF-файлов. файл YellowPadWindow.File.cs обеспечивает работу всех четырех команд меню File. Чтобы программа сохраняла разумные размеры, я решил не сохранять имя загруженного файла и не выдавать запрос о несохраненных изменениях. Коман- ды Open и Save в основном реализуют операции с файлами в формате ISF. YellowPadWindow.File.cs // // YellowPadW1ndow.F11e.es (с) 2006 by Charles Petzold и using Microsoft.Win32; using System; using System.10; using System.Windows; using System.Windows.Ink: using System.Windows.Input; using System.Windows.Markup: using System.Windows.Media; namespace Petzold.YellowPad { public partial class YellowPadWindow : Window { // Команда File>New: удаление всех штрихов void NewOnExecuted(object sender, ExecutedRoutedEventArgs args) { inkcanv.Strokes.Cl ear(): // Команда File>Open: вызов OpenFileDialog и загрузка ISF-файла void OpenOnExecuted(object sender, ExecutedRoutedEventArgs args) OpenFileDialog dig = new OpenFI1eDIalog(); dlg.CheckFi1eExIsts = true: dig.Filter = "Ink Serialized Format (*.isf)|*.isf|" + "All files (*.*)|*.*": if ((bool)dlg.ShowDialog(this)) try { FileStream file = new FileStream(dlg.FileName, FileMode.Open, Fi1 eAccess.Read): inkcanv.Strokes = new StrokeCollection(file);
572 Г лава 22. Окна, страницы и перемещение fi1е.Close(); } catch (Exception exc) { MessageBox.Show(exc.Message. Title); } } } // Команда File>Save: вызов окна SaveFileDialog void SaveOnExecuted(object sender, ExecutedRoutedEventArgs args) SaveFileDialog dig = new SaveFileDialog(); dig.Filter = "Ink Serialized Format (*.isf)|*.isf|" + "XAML Drawing File (*.xaml)|*.xaml|" + "All files (*.*)|*.*"; if ((bool)dlg.ShowDialog(this)) { try { FileStream file = new FileStream(dlg.FileName, FileMode.Create. FileAccess.Write); if (dig.Filterindex == 1 || dig.Filterindex == 3) i nkcanv.Strokes.Save(fi1e); else // Сохранение штрихов в объекте DrawlngGroup DrawingGroup drawgrp = new DrawlngGroup(): foreach (Stroke strk in inkcanv.Strokes) { Color clr = strk.DrawingAttributes.Color; i f (strk.DrawlngAttributes.IsHighlighter) clr = Color.FromArgb(128, clr.R, clr.G, clr.B): drawgrp.Children.Add( new GeometryDrawing( new SolidColorBrush(clr), null. strk.GetGeometryO)); } XamlWriter,Save(drawgrp. file): } file.Close(); } catch (Exception exc) { MessageBox.Show(exc.Message, Title): } } }
Окна, страницы и перемещение 573 // Команда File>Exit: закрытие окна void CloseOnExecuted(object sender, ExecutedRoutedEventArgs args) { CloseO; } } } Стоит заметить, что SaveFileDialog также включает команду сохранения штри- хов в формате «XAML Drawing File». На данной функции стоит задержаться подробнее. Меня всегда интересовали возможности использования ввода со стилуса в гра- фическом программировании. В этом нам поможет класс Stroke, в котором опре- деляется метод GetGeometry, возвращающий объект типа Geometry. Этот объект будет описан в главе 28, а пока достаточно знать, что в наиболее обобщенной форме объект Geometry напоминает традиционную графическую траекторию — совокупность соединенных и несоединенных отрезков и кривых. Я с большим интересом узнал, что объект Geometry, возвращаемый методом GetGeometry, не сводится к определению ломаной, описывающей штрих. Фактически мы полу- чаем контур построенного штриха с учетом формы и размеров наконечника стилуса и силы нажатия на экран. Класс Geometry содержит геометрические данные — точки, линии и кривые. Geometry в сочетании с кистью контура и кистью заполнения дает объект типа GeometryDrawing, поэтому дополнительный код метода SaveOnExecuted использует объект DrawingAttributes каждого объекта Stroke для заполнения геометрических данных соответствующим цветом. Каждый объект GeometryDrawing соответствует объекту Stroke; эти объекты объединяются в объект DrawingGroup, который со- храняется программой в файле методом XamlWriter.Save. (Классы GeometryDrawing и DrawingGroup являются производными от абстрактного класса Drawing; эти кон- цепции более подробно рассматриваются в главе 31.) Итак, теперь мы имеем объект DrawingGroup в файле. Что с ним можно сде- лать? На базе любого объекта типа Drawing можно создать объект Drawingimage: Drawinglmage drawimg = new Drawinglmage(drawing): Это уже интереснее, потому что Drawingimage наследует от ImageSource, a Image- Source — тип свойства Source, определяемого классом Image. Мы впервые встре- тились с этим классом в программе ShowMyFace в главе 3. Хотя класс Image обычно используется для отображения растровой графики, он также может ра- ботать и с векторной графикой, если она хранится в объектах типа Drawingimage. Итак, любое изображение, нарисованное в программе YellowPag, можно сохра- нить в формате, легко отображаемом в виде графического объекта WPF. Далее в меню верхнего уровня следует команда Edit, но мы пока пропустим ее и вернемся к ней позднее. После Edit следуют команды Stylus-Mode и Eraser- Mode. Первая определяет, что должно происходить при рисовании на экране на- конечником стилуса или мышью, и соответствует свойству EditingMode объекта Ink- Canvas. «Режим ластика» активизируется тогда, когда пользователь переворачивает
574 Глава 22. Окна, страницы и перемещение стилус и рисует другим концом. Он соответствует свойству EditingModelnverted объекта InkCanvas. Так как этот режим имеет смысл только на планшетных ком- пьютерах, при запуске на других компьютерах команда Eraser-Mode исключается из меню. Свойствам EditingMode и EditingModelnverted задаются члены перечисления Ink- CanvasEditingMode. По умолчанию EditingMode задается значение InkCanvasEditing- Mode.Ink, a EditingModelnverted — InkCanvasEditingMode.EraseByStroke (штрих стира- ется полностью при стирании любой его части). Для полноты картины (а также для того, чтобы штрихи можно было стирать мышью), я разместил эти же ко- манды в меню Stylus-Mode и Eraser-Mode. Кроме Ink и EraseByStroke, эти меню по- зволяют выбрать режимы EraseByPoint (вместо удаления штрих разбивается на- двое) и Selection. В последнем режиме один пли несколько штрихов выделяются при помощи «лассо». Реализации команд меню Stylus-Mode и Eraser-Mode в файле YellowPadWindow. Mode.cs очень похожи. При открытии подменю программа устанавливает помет- ку команды, соответствующей текущему значению InkCanvas. Обработчики Click применяют выделенный режим к объекту InkCanvas. YellowPadWindow.Mode.cs //------------------------------------------------------ // YellowPadWindow.Mode.cs (c) 2006 by Charles Petzold //------------------------------------------------------ using System; using System.Windows: using System.Windows.Controls; namespace Petzold.YellowPad { public partial class YellowPadWindow : Window { // Открыто подменю Stylus-Mode: пометка одной из команд void StylusModeOnOpened(object sender, RoutedEventArgs args) Menuitem item = sender as Menuitem: foreach (Menuitem child in item.Items) child.IsChecked = inkcanv.EditingMode == (InkCanvasEditingMode)child.Tag: } // Задание свойства EditingMode в зависимости от выбора команды void StylusModeOnClick(object sender, RoutedEventArgs args) { Menuitem item = sender as Menuitem; inkcanv.EditingMode = (InkCanvasEditingMode)item.Tag: } // Открыто подменю Eraser-Mode: пометка одной из команд void EraserModeOnOpened(object sender, RoutedEventArgs args)
Окна, страницы и перемещение 575 { Menuitem item = sender as Menuitem; foreach (Menuitem child in item.Items) child.IsChecked = inkcanv.EditingModelnverted == (InkCanvasEdi ti ngMode)chi1d.Tag; } // Задание свойства EditingModelnverted в зависимости // от выбора команды void EraserModeOnClick(object sender, RoutedEventArgs args) { Menuitem item = sender as Menuitem; inkcanv.EditingModelnverted = (InkCanvasEditingMode)item.Tag; } } } Установка пометки на основании текущего значения InkCanvas — самый без- опасный вариант, потому что значение EditingMode может измениться и без вы- зова меню. Например, при вызове метода Select объекта InkCanvas EditingMode пе- реходит в режим Select. Ранее я уже упоминал, что объект InkCanvas содержит свойство DefaultDraw- ingAttributes типа DrawingAttributes, в котором хранятся данные о цвете, размерах и форме наконечника стилуса, применяемые ко всем будущим штрихам. Класс DrawingAttributes также содержит два логических свойства с именами IgnorePres- sure и IsHighlighter. При установке первого флага InkCanvas игнорирует силу на- жатия стилуса при рисовании. С установленным флагом IsHighlighter к выбран- ному цвету применяется альфа-канал 128, в результате чего цвет становится полупрозрачным. Ластик не обладает столь широкими возможностями — вы можете менять только его размеры и форму. Свойство EraserShape класса InkCanvas относится к типу StylusShape — это абстрактный класс со свойствами Height, Width и Rotation. На базе StylusShape создаются производные классы EllipseStylusShape и Rectangle- StylusShape. Несмотря на различия в работе со стилусом и ластиком, я решил построить их интерфейс на базе одного диалогового окна. Это окно, определяемое в сле- дующем XAML-файле, содержит элементы управления, соответствующие боль- шинству свойств DrawingAttributes. StylusToolDialog.xaml < I.. =================================================== StylusToolDialog.xaml (с) 2006 by Charles Petzold <Window xml ns="http;//schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:color="clr-namespace;Petzold.Li stColorsElegantly" x:Class="Petzold.Yel1owPad.Sty1 us Tool Dialog"
576 Глава 22. Окна, страницы и перемещение Title="Stylus Tool" ShowInTaskbar="False" WindowStartupLocation="CenterOwner" Si zeToContent="Wi dthAndHei ght" Res i zeMode="NoRes i ze" > <Grid Margin="6"> <Gr1d.ColumnDefi ni ti ons> <ColumnDefinition /> <ColumnDefinition /> </Gri d.ColumnDefi ni ti ons> <Gri d.RowDefi ni ti ons> <RowDefinition /> <RowDefinition /> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Gri d.RowDefi ni ti ons> <!-- Сетка Grid 3x4 содержит три объекта TextBox --> <Grid Grid.Row="0" Grid.Column="0"> <Gri d.RowDefi ni t i ons> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Gri d.ColumnDefi ni ti ons> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> </Gri d.ColumnDefi ni ti ons> <Label Content="_Width:" Grid.Row="0" Grid.Column="0" Margin="6 6 0 6" /> <TextBox Name="txtboxWidth" Grid.Row="0" Grid.Column="l" Width="50" TextAlignment="Right" Margin="0 6 0 6" /> <Label Content="points" Grid.Row="0" Grid.Column="2" Margin="0 6 6 6" /> <Label Content="_Height:" Grid.Row="l" Grid.Column="0" Margin="6 6 0 6" /> <TextBox Name="txtboxHeight" Grid.Row="l" Grid.Column="l" Width="50" TextAlignment="Right" Margin="0 6 0 6" /> <Label Content="points" Grid.Row="l" Grid.Column="2" Margin="0 6 6 6" /> <Label Content="_Rotation:" Grid.Row="2" Grid.Column="0" Margin="6 6 0 6" />
Окна, страницы и перемещение 577 <TextBox Name="txtboxAngle" Grid.Row="2" Grid.Column='T' Width="50" TextAlignment="Right" Margin="0 6 0 6" /> <Label Content="degrees" Grid.Row="2" Grid.Column="2" Margined 6 6 6” /> </Grid> <!-- GroupBox с двумя элементами RadioButton для наконечника стилуса --> <GroupBox Header="_Stylus Tip" Grid.Row="l" Grid.Column="0" Margin="6"> <StackPanel> <RadioButton Name="radioEllipse" Content="Ellipse" Margin="6" /> <RadioButton Name="radioRect" Content="Rectangle" Margin="6" /> </StackPanel> </GroupBox> <!-- Два элемента CheckBox для флагов --> <CheckBox Name=”chkboxPressure" Content="_Ignore pressure" Grid.Row="2" Grid.Column="0" Margin="12 6 6 6" /> <CheckBox Name="chkboxHighlighter" Content="_Highlighter" Grid.Row="3" Grid.Column="0" Margin="12 6 6 6" /> <!-- ColorListBox из проекта ListColorsElegantly --> <color:ColorListBox x:Name="lstboxColor" Width="150" Height="200" Grid.Row="0" Grid.Column="l" Grid.RowSpan="3" Margin="6"/> <!-- Кнопки OK и Cancel --> <UniformGrid Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" Columns="2"> <Button Content="0K" Name="btnOk" Click="OkOnClick" IsDefault="True" MinWidth="60" Margin="6" HorizontalAlignment="Center" /> <Button Content="Cancel" IsCancel="True" MinWidth="60" Margin="6" HorizontalAlignment="Center" /> </UniformGrid> </Grid> </Window> Обратите внимание: элемент ColorListBox позаимствован из программы ListColors- Elegantly, приведенной в главе 13, а XAML-файл должен содержать объявление пространства имен XML этого проекта. В файле StylusToolDialog.cs определяется оставшаяся часть класса StylusToolDialog. Свойство DrawingAttributes инициализирует элементы в методе доступа set и со- здает новый объект DrawingAttributes по значениям элементов в методе доступа get. Ширина и высота наконечника выводятся в диалоговом окне в пунктах (1/72 дюй- ма), поэтому методы доступа содержат соответствующие преобразования.
578 Глава 22. Окна, страницы и перемещение StylusToolDialog.cs // // StylusToolDialog.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; using System.Windows.Ink; using System.Windows.Media: namespace Petzold.YellowPad { public partial class StylusToolDialog : Window { // Конструктор public StylusToolDialog() { InitializeComponentO; // Задание обработчиков событий для снятия блокировки // с кнопки ОК txtboxWidth.TextChanged += TextBoxOnTextChanged; txtboxHeight.TextChanged += TextBoxOnTextChanged; txtboxAngle.TextChanged += TextBoxOnTextChanged; txtboxWidth.Focus(); } // Открытое свойство инициализирует элементы // и возвращает их значения public DrawingAttributes DrawingAttributes { set { txtboxHeight.Text = (0.75 * value.Height) .ToStringCFl"): txtboxWidth.Text = (0.75 * value.Width) .ToStringCFl"); txtboxAngle.Text = (180 * Math.Acos(value.StylusTipTransform.Mil) / Math.PI).ToStringCFl"); chkboxPressure.IsChecked = value.IgnorePressure; chkboxHighlighter.IsChecked = value.IsHighlighter: if (value.StylusTip == StylusTip.Ellipse) radioEllipse.IsChecked = true; else radioRect.IsChecked = true; IstboxColor.SelectedColor = value.Color; 1stboxColor.Scrol11ntoVi ew(1stboxColor.SeiectedColor); }
Окна, страницы и перемещение 579 get { DrawingAttributes drawattr = new DrawingAttributesO: drawattr.Height = Double.Parse(txtboxHeight.Text) / 0.75; drawattr.Width = Double.Parse(txtboxWidth.Text) / 0.75; drawattr.StylusTipTransform = new RotateTransform(Double.Parse(txtboxAngle.Text)).Vaiue; drawattr.IgnorePressure = (bool)chkboxPressure.IsChecked; drawattr.IsHighlighter = (bool)chkboxHighlighter.IsChecked; drawattr.StylusTip = (bool)radioEllipse.IsChecked ? StylusTip.Ellipse : StylusTip.Rectangle; drawattr.Color = 1stboxColor.SelectedColor: return drawattr; } } // Обработчик события снимает блокировку с кнопки ОК // только в том случае, если все поля содержат действительные данные void TextBoxOnTextChanged(object sender, TextChangedEventArgs args) { double width, height, angle; btnOk.IsEnabled = Double.TryParse(txtboxWidth.Text, out width) && width / 0.75 >= DrawingAttributes.MinWidth && width / 0.75 <= DrawingAttributes.MaxWidth && Double.TryParse(txtboxHeight.Text, out height) && height / 0.75 >= DrawingAttributes.MinHeight && height / 0.75 <= DrawingAttributes.MaxHeight && Double.TryParse(txtboxAngle.Text, out angle); } // Кнопка OK уничтожает диалоговое окно void OkOnClick(object sender, RoutedEventArgs args) { DialogResult = true; } } } Вызов Double.Parse в методе доступа get безопасен, потому что кнопка ОК диа- логового окна становится доступной только в том случае, если все три элемента TextBox содержат действительные значения double. Соответствующая логика на- ходится в обработчике события TextChanged, общем для всех трех элементов TextBox. Класс EraserToolDialog наследует от StylusToolDialog. Его конструктор назначает диалоговому окну новый заголовок и скрывает три элемента со свойствами, не поддерживаемые в режиме ластика.
580 Глава 22. Окна, страницы и перемещение EraserToolDialog.cs //-------------------------------------------------- // EraserToolDialog.cs (с) 2006 by Charles Petzold // using System: using System.Windows: using System.Windows.Controls: using System.Windows.Ink; namespace Petzold.YellowPad public class EraserToolDialog : StylusToolDialog { // Конструктор скрывает неактуальные элементы в StylusToolDialog public EraserToolDialog() { Title = "Eraser Tool"; chkboxPressure.Visibility = Visibility.Collapsed: chkboxHighlighter.Visibility *-= Visibility.Collapsed: IstboxColor.Visibility = Visibility.Collapsed: // Открытое свойство инициализирует элементы // и возвращает их значения public StylusShape EraserShape { set { txtboxHeight.Text = (0.75 * value.Height) .ToStringCFl"); txtboxWidth.Text = (0.75 * value.Width) .ToStringCFl"): txtboxAngle.Text = value.Rotation.ToString(): if (value is EllipseStylusShape) radioEllipse.IsChecked = true; else radioRect.IsChecked = true: } get { StylusShape eraser; double width = Double.Parse(txtboxWidth.Text) / 0.75: double height = Double.Parse(txtboxHeight.Text) / 0.75: double angle = Double.Parse(txtboxAngle.Text): if ((bool)radioEllipse.IsChecked) eraser = new EllipseStylusShape(width, height, angle); else eraser = new RectangleStylusShape(width, height, angle): return eraser: } } }
Окна, страницы и перемещение 581 Класс определяет новое свойство с именем EraserShape типа StylusShape (ана- лог свойства EraserShape, определяемым InkCanvas), а код в общих чертах повто- ряет код свойства DrawingAttributes класса StylusToolDialog. Меню Tools содержит две команды: Stylus и Eraser. Файл YellowPadWindow.Tools.cs обеспечивает вызов окон StylusToolDialog и EraserToolDialog при выборе этих команд. YellowPadWindow.Tools.cs и // YellowPadWindow.Tools.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; namespace Petzold.YellowPad { public partial class YellowPadWindow : Window { // Вызов StylusToolDialog и использование свойства DrawingAttributes void StylusToolOnClick(object sender, RoutedEventArgs args) StylusToolDialog dig = new StylusToolDialog(); dig.Owner = this; dig.DrawingAttributes = inkcanv.DefaultDrawingAttributes; i f ((bool)dlg.ShowDia 1og().GetVa1ueOrDefault()) inkcanv.DefaultDrawingAttributes = dig.DrawingAttributes; } } // Вызов EraserToolDialog и использование свойства EraserShape void EraserToolOnClick(object sender. RoutedEventArgs args) { EraserToolDialog dig = new EraserToolDialog(); dig.Owner = this; dig.EraserShape = inkcanv.EraserShape; if ((bool)dlg.ShowDia 1og().GetVa1ueOrDefault()) { inkcanv.EraserShape = dig.EraserShape; } } } А теперь вернемся к меню Edit. Как упоминалось ранее, пользователь мо- жет выбрать в меню Stylus-Mode команду Select и выделить один или несколько штрихов при помощи «лассо». Класс InkCanvas содержит методы CopySelection и CutSelection для копирования выделенных штрихов в буфер; метод CutSelection
582 Глава 22. Окна, страницы и перемещение также удаляет выделенные штрихи из коллекции. В классе InkCanvas также оп- ределяется метод CanPaste, который проверяет наличие в буфере штрихов, а так- же метод Paste, вставляющий их в InkCanvas. Как показывает следующий файл, стандартные команды меню Edit реализуются достаточно просто. YellowPadWindow.Edit.cs //----------------------------------------------------- // Yel1owPadWindow.Edit.cs (c) 2006 by Charles Petzold //----------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Ink; using System.Windows.Input; namespace Petzold.YellowPad public partial class YellowPadWindow : Window { // Команда Format становится доступной при наличии // выделенных штрихов void EditOnOpenedfobject sender. RoutedEventArgs args) { itemFormat.IsEnabled = inkcanv.GetSelectedStrokes().Count > 0: } // Команды Cut. Copy. Delete становятся доступными // при наличии выделенных штрихов void CutCanExecuteCobject sender. CanExecuteRoutedEventArgs args) { args.CanExecute = Inkcanv.GetSelecteciStrokesO .Count > 0; } // Реализация команд Cut и Copy с использованием методов InkCanvas void CutOnExecuted(object sender, ExecutedRoutedEventArgs args) inkcanv.CutSelectionO ; void CopyOnExecuted(object sender, ExecutedRoutedEventArgs args) inkcanv.CopySelectionO; } // Команда Paste становится доступной, если содержимое буфера // подходит для InkCanvas void PasteCanExecuteCobject sender. CanExecuteRoutedEventArgs args) {
Окна, страницы и перемещение 583 args.CanExecute = Inkcanv.CanPasteO; } // Реализация Paste с использованием метода InkCanvas void PasteOnExecutedCobject sender, ExecutedRoutedEventArgs args) { inkcanv.PasteO: } // Команда Delete реализуется "вручную" void DeleteOnExecutedCobject sender, ExecutedRoutedEventArgs args) { foreach (Stroke strk in inkcanv.GetSelectedStrokesO) i nkcanv.Strokes.Remove(strk): ) // Команда Select All: выделение всех штрихов void SelectA110nExecuted(object sender. ExecutedRoutedEventArgs args) inkcanv.Seiect(inkcanv.Strokes); } // Команда Format Selection: вызов StylusToolDialog void FormatOnClick(object sender, RoutedEventArgs args) StylusToolDialog dig = new StylusToolDialogO: dig.Owner = this: dig.Title = "Format Selection": // Пытаемся получить DrawingAttributes для первого // выделенного штриха Strokecollection strokes = inkcanv.GetSelectedStrokesO: if (strokes.Count > 0) dig.DrawingAttributes = strokes[0].DrawingAttributes; else dig.DrawingAttributes = inkcanv.DefaultDrawingAttributes: if ((bool )dlg.ShowDialog() .GetValueOrDefaultO) // Задание DrawingAttributes для всех выделенных штрихов foreach (Stroke strk in strokes) strk.DrawingAttributes = dig.DrawingAttributes; } } } Для снятия блокировки с команд, для которых необходимо наличие выде- ленных штрихов, я использую метод GetSelectedStrokes объекта InkCanvas, возвра- щающий объект типа Strokecollection.
584 Глава 22. Окна, страницы и перемещение Сначала я не собирался реализовывать команду изменения форматирования выделенных штрихов, но потом понял, что задача в основном решается все тем же окном StylusToolDialog с новым свойством Title. Код в конце файла инициали- зирует диалоговое окно на основании свойства DrawingAttributes первого выде- ленного штриха, а затем задает форматирование всех выделенных штрихов по новому объекту DrawingAttributes, созданному диалоговым окном. Файл YellowPadWindow.Help.cs реализует две команды меню Help. Обе команды отображают на экране диалоговые окна. Команда Help вызывает немодальное диалоговое окно типа YellowPadHelp, а команда About — модальное диалоговое окно типа YellowPadAboutDialog. YellowPadWindow.Help.cs //----------------------------------------------------- // YellowPadWIndow.Help.cs (c) 2006 by Charles Petzold //----------------------------------------------------- using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input: namespace Petzold.YellowPad public partial class YellowPadWindow : Window // Команда Help: вызов диалогового окна YellowPadHelp // в немодальном режиме void HelpOnExecuted(object sender. ExecutedRoutedEventArgs args) { YellowPadHelp win = new YellowPadHelpO; win.Owner = this: win.ShowO: } // Команда About: вызов диалогового окна YellowPadAboutDialog void AboutOnClick(object sender. RoutedEventArgs args) YellowPadAboutDialog dig = new YellowPadAboutDialogO: dig.Owner = this: dlg.ShowDialogO: } } } Описание класса YellowPadHelp немного откладывается — до тех пор, пока не будет изложен весь необходимый материал. Следующий файл YellowPadAbout- Dialog.xaml определяет макет диалогового окна About.
Окна, страницы и перемещение 585 YellowPadAboutDialog.xaml < ! - - = = = === = === === = = = = = = = = === = = = = ===== = === = = = = = === = ==; Yel1owPadAboutDialog.xaml (c) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Class="Petzold.Yel1owPad.Yel1owPadAboutD!a1og" Title="About YellowPad" ShowInTaskbar="False" WindowStartupLocation="CenterOwner" SizeToContent="WidthAndHeight" ResizeMode="NoResi ze"> <StackPanel> <!-- Название программы --> <TextBlock Horizontal Alignment="Center" Margin="12" FontSize="48"> <1taiic>Yel1ow Pad</Italic> </TextBlock> <!-- Обложка книги, для которой написана программа --> <Image Source="Images\BookCover.jpg" Stretch="None" Margin="12" /> <!-- Другой элемент Image для файла с подписью/информацией об авторских правах --> <Image Name="imgSignature" Stretch="None" Margin="12" /> <!-- Ссылка на сайт --> <TextBlock Horizontal Alignment="Center" FontSize="20"> <Hyperlink NavigateUri="http://www.charlespetzold.com" RequestNavigate="LinkOnRequestNavigate"> www.charlespetzold.com </Hyperlink> </TextBlock> <!-- Кнопка OK одновременно является кнопкой по умолчанию и кнопкой отмены --> <Button HorizontalAlignment^'Center" MinWidth="60" Margin="12" IsDefault="True" IsCancel="True"> OK </Button> </StackPanel> </Window> Файл содержит два элемента Image. Первый загружает из каталога Images про- екта ресурсный файл с изображением обложки книги. Второй элемент Image не содержит свойства Source, но имеет свойство Name со значением "imgSignature”. Свойство Source для этого элемента задается в коде C# следующего файла Yellow- PadAboutDialog.cs. Я создал файл Signature.xaml в программе YellowPad и включил
586 Глава 22. Окна, страницы и перемещение его в проект YellowPad с типом Resource. Код конструктора YellowPadAboutDialog получает объект Stream для этого ресурса и преобразует его в объект типа Drawing методом XamIReader.Load. Свойству Source второго элемента Image в XAML-фай- ле задается объект Drawingimage, основанный на объекте Drawing. YellowPadAboutDialog.cs //------------------------------------------------------ // YellowPadAboutDialog.cs (с) 2006 by Charles Petzold //------------------------------------------------------ using System; using System.Diagnostics; // для класса Process using System.10: using System.Windows; using System.Windows.Media; using System.Windows.Markup; using System.Windows.Navi gation; // для RequestNavigateEventArgs namespace Petzold.YellowPad public partial class YellowPadAboutDialog { public YellowPadAboutDialogO { InitializeComponentO; // Загрузка объекта Drawing с подписью/информацией // об авторских правах и задание элемента Image Uri uri = new Uri("pack://application:,.,/Images/Signature.xaml"); Stream stream = Application.GetResourceStream(uri).Stream; Drawing drawing = (Drawing)XamlReader.Load(stream); stream.CloseO; imgSignature.Source = new Drawinglmage(drawing); // Гиперссылка открывает веб-сайт void LinkOnRequestNavigateCobject sender, RequestNavigateEventArgs args) { Process.Start(args.Uri.Originalstring); args.Handled = true; } } Также обратите внимание на элемент Hyperlink в файле YellowPadAboutDialog.xaml. Если щелкнуть на ссылке, мой веб-сайт откроется в браузере по умолчанию. Мы уже использовали эту возможность ранее — класс AboutDialog в программе
Окна, страницы и перемещение 587 Notepadclone из главы 18 определяет аналогичный элемент Hyperlink, но уста- навливает для его события Click следующий обработчик: void LinkOnClick(object sender. RoutedEventArgs args) { Process.Start("http://www.charlespetzold.com"); Файл YellowPadAboutDialog.xaml вместо этого задает свойству NavigateUri адрес для перехода, а свойству RequestNavigate — обработчик события: <Ну ре г 11 nk NavigateUri =" http: //www. cha rl espetzol d. com" RequestNavigate="LinkOnRequestNavigate"> vmvj .charl espetzol d. com </Hyperlink> Обработчик LinkOnRequestNavigate в файле YellowPadAboutDialog.es выглядит так: void LinkOnRequestNavigate(object sender. RequestNavigateEventArgs args) Process.Start(args.Uri.Originalstring); args.Handled = true: } Обработчик события передает адрес URI, заданный свойству NavigateUri, ме- тоду Process.Start. Код обработчика события становится чуть более универсаль- ным, по в остальном он выглядит неоправданно «раздутым» по сравнению с ком- пактным решением, использующем Click. Казалось бы, если передать элементу Hyperlink в XAML-файле адрес, который требуется открыть, элемент должен сам открыть ссылку без дополнительного кода. Однако если удалить атрибут RequestNavigate из элемента Hyperlink и пере- компилировать программу, при щелчке па ссылке ничего не происходит. А что, собственно, должно происходить? Должен ли элемент Hyperlink запустить брау- зер по умолчанию, как это делает Process.Start? Или запрашиваемая веб-страни- ца должна заменить все содержимое окна About? Если второй вариант вам по- нравился, вам повезло — Hyperlink позволяет это сделать. Для этого потребуется лишь указать подходящее окно. Попробуйте сделать следующее: в окне YellowPadAboutDialog.xaml замените оба вхождения Window на Navigationwindow и заключите StackPanel в элемент свойства NavigationWindow.Content. Navigationwindow — единственный определенный в WPF класс, производный от Window. Теперь удалите атрибут события RequestNavigate из элемента Hyperlink (если это не было сделано ранее) и перекомпилируйте программу. Теперь информация About выводится в окне несколько иного вида. У верхнего края находятся две заблокированные кнопки со стрелками. Если щелкнуть на ссылке, то содержимое About заменяется содержимым сайта, а кноп- ка возврата становится доступной. Щелчок на ней возвращает прежнее содер- жимое About. Добро пожаловать в мир навигационных приложений WPF! Как было пока- зано ранее, элемент Hyperlink обычно работает только при установленном обра- ботчике события. Но если он находится в окне Navigationwindow (или в элементе
588 Глава 22. Окна, страницы и перемещение Frame), щелчок на ссылке автоматически открывает страницу, заданную свойст- вом NavigateUri. Свойство NavigateUri может ссылаться на URI веб-сайта, но чаще в нем содержится имя другого XAML-файла в программе. В сущности, использование Navigationwindow и Frame позволяет структуриро- вать все приложение WPF как веб-сайт, но с сохранением всех возможностей WPF: элементов управления, интерфейсных элементов и графики. Проект NavigationDemo демонстрирует основные приемы навигации. Проект состоит из пяти XAML-файлов и одного файла С#. Первый XAML-файл содер- жит определение приложения. NavigationDemoApp.xaml < ।_ _ ==================================================== NavigationDemoApp.xaml (с) 2006 by Charles Petzold Application xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentati on" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Class="Petzold.NavigationDemo.MyApp" StartupUri="NavigationDemoWindow.xaml" /> Атрибут StartupUri означает, что содержимое NavigationDemoWindow.xaml должно загружаться в исходном окне приложения. У корневого элемента NavigationDemo- Window.xaml имеется атрибут Source, в котором указан другой XAML-файл. NavigationDemoWindow.xaml < !_ _ ======================================================= NavigationDemoWindow.xaml (с) 2006 by Charles Petzold <NavigationWindow xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentati on" Title="Navigation Demo" FontSize="24" Source="Pagel.xaml" /> Source — одно из нескольких свойств, определяемых классом Navigationwindow в дополнение к свойствам, унаследованным от Window. Класс Frame тоже обладает свойством Source. Как Navigationwindow, так и Frame наследуют от Contentcontrol, но если задать любому из этих элементов свойство Content, оно будет обла- дать более высоким приоритетом, чем Source. Обычно элементы Navigationwindow и Frame используются ради своих навигационных возможностей, поэтому вни- мание стоит сосредоточить на свойстве Source, а не Content. Свойство Source предшествующего элемента Navigationwindow ссылается на файл Pagel.xaml. Pagel.xaml < ।_ _ ======================================== Pagel.xaml (c) 2006 by Charles Petzold
Окна, страницы и перемещение 589 < Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Title="Page 1" WindowTitle="Navigation Demo: Page 1"> <TextBlock Horizontal Alignment="Center" VerticalAlignment="Center"> Go to <Hyperlink NavigateUri=,,Page2.xaml,,>Page 2</Hyperlink>. </TextBlock> </Page> Свойству Source объекта Navigationwindow или Frame не обязательно зада- вать XAML-файл с корневым элементом Раде. (А в программном коде свойству Source не обязательно задавать объект типа Раде.) Тем не менее Раде обладает рядом полезных особенностей, благодаря которым он хорошо подходит для на- вигационных приложений. Две из них представлены в файле Pagel.xaml. Свой- ство Title определяет текст, который выводится в списках посещенных страниц, вызываемых кнопками перемещения; списки упрощают возврат к ранее посе- щенным страницам. Свойство WindowTitle замещает свойство Title объекта Navi- gationWindow. (Еще один показатель того, что объект Раде специально проекти- ровался для навигационных приложений: родителем элемента Раде может быть только Navigationwindow или Frame.) В остальном Pagel.xaml просто содержит объект TextBlock с встроенным эле- ментом Hyperlink, свойство NavigateUri которого равно Page2.xamL Pagel.xaml < । _ _ ======================================== Page2.xaml (с) 2006 by Charles Petzold < Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Class="Petzold.Navi gati onDemo.Page2" Title="Page 2" WindowTitle="Navigation Demo: Page 2"> <Grid> <Gri d.RowDefi ni ti ons> <RowDefinition Height="33*" /> <RowDefinition Height="33*" /> <RowDefinition Height="33*" /> </Grid.RowDefi ni ti ons> <TextBlock Grid.Row="0" Horizontal Alignment="Center" VerticalAlignment="Center"> RequestNavigate event handled for navigation to <Hyperlink NavigateUri="Page3.xaml" RequestNavigate="HyperlinkOnRequestNavigate"> Page 3</Hyperlink>. </TextBlock> <Button Grid.Row="l" HorizontalAlignment="Center" Vertical Alignment="Center" Click="ButtonOnClick">
590 Глава 22. Окна, страницы и перемещение Click to go to Page 1 </Button> <TextBlock Grid.Row="2" Horizontal Alignment="Center" Vertical Alignment="Center"> Go to <Hyperlink NavigateUri="http://www.charlespetzold.com"> Petzold's Web site</Hyperlink>. </TextBlock> </Grid> </Page> Элемент Page в файле Page2.xaml получился более сложным, чем в первой странице. Он содержит два элемента TextBlock и один элемент Button, причем все элементы помечены так, как если бы они содержали активные ссылки. Первый объект TextBlock содержит встроенный элемент Hyperlink, задающий обработчик события RequestNavigate. У элемента Button задается обработчик его события Click. Третий объект Hyperlink содержит только свойство NavigateUri, но оно ссылается на мой веб-сайт, а не на локальную страницу XAML. Page2.xaml необходим файл программной логики с обработчиками событий. Именно этим объясняется присутствие атрибута x:Class, который не нужен Pagel, xaml. Приведенный далее файл программной логики содержит вызов InitializeCom- ponent в конструкторе для подключения обработчиков событий, а также код са- мих обработчиков событий. Page2.cs //--------------------------------------- // Page2.cs (с) 2006 by Charles Petzold //--------------------------------------- using System: using System.Windows: using System.Windows.Controls: using System.Windows.Documents: using System.Windows.Navigation: namespace Petzold.NavigationDemo { public partial class Page2 { public Page2() InitializeComponentO: } void ButtonOnClick(object sender, RoutedEventArgs args) Navigationservice.NavigateC new Uri("Pagel.xaml", UriKi nd.Relative)):
Окна, страницы и перемещение 591 void Hyperl1nkOnRequestNavlgate(object sender, RequestNavigateEventArgs args) { Navigationservice.NavigateCargs.Uri); args.Handled = true: } } } Метод ButtonOnClick должен сообщить Navigationwindow о переходе к файлу Pagel.xaml; для этого он вызывает метод Navigate, определяемый в Navigation- Window. Тем не менее класс Раде2 не может напрямую обратиться к объекту Navigationwindow с методом Navigate. Первый возможный способ получения Na- vigationWindow основан на использовании свойства Application.Current, а также свой- ства MainWindow класса Application: NavigationWindow navwin = (Application.Current.MainWindow as Navigationwindow): После этого можно вызвать метод Navigate для объекта navwin, чтобы перейти к нужному файлу. Впрочем, в файле Page2.cs продемонстрирован более простой способ. Класс Раде определяет свойство Navigationservice, которое само обладает методом Navigate. Объект Navigationservice предоставляет доступ к навигационным возможностям объекта Navigationwindow, частью которых является Раде. Сущест- вование свойства Navigationservice — еще одна причина, по которой Раде хорошо подходит для навигационных приложений. Впрочем, если вам когда-либо потре- буется организовать навигацию в другом классе, отличном от Раде, вы можете получить объект Navigationservice при помощи статического метода Navigationservice. GetNavigationService, но при этом класс, вызывающий Navigate, по-прежнему дол- жен находиться внутри Navigationwindow или Frame. При вызове метода Navigate указывается URI или объект. В методе Button- OnClick второй аргумент конструктора Uri означает, что путь, заданный для фай- ла Pagel.xaml, задается относительно пути текущего XAML-файла, которым яв- ляется Page2.xaml. Обработчик события RequestNavigate реализует задачу навигации аналогичным образом, если не считать того, что URI-адрес, передаваемый обработчиком, мето- ду Navigate, совпадает с тем, который был изначально задан в элементе Hyperlink файла Page2.xaml. Обработчик события устанавливает свойство Handled равным true; это означает, что навигация выполнена и ничего более делать не требуется. В данном примере перемещение вызова Navigate в обработчик события ниче- го не дает по сравнению с его автоматическим выполнением элементом Hyperlink. Однако в другой ситуации, возможно, потребуется проанализировать ссылку перед переходом. Последний файл проекта РадеЗ.хат! не сложнее Pagel.xaml. В начале файла определяется элемент TextBox, а в конце — ссылка для возвращения к Pagel.xaml.
592 Глава 22. Окна, страницы и перемещение РадеЗ.хат! <!- - ======================================== РадеЗ.хат! (с) 2006 by Charles Petzold <Раде xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Title="Page 3" WindowTitle="Navigation Demo: Page 3"> <Navi gati onWi ndow.Content> <Grid> <Gri d.RowDefi ni ti ons> <RowDefinition Height="50*" /> <RowDefinition Height="50*" /> </Grid.RowDefi ni ti ons> <TextBox Grid.Row="0" MinWidth="2in" Margin="48" HorizontalAlignment="Center" Vertical Alignment="Top" /> <TextBlock Grid.Row="2" Margin="48" Hori zonta1Ali gnment="Ri ght" Verti ca1Ali gnment="Bottom"> Go back to <Hyperlink NavigateUri="Pagel.xaml"> Page 1</Hyperlink>. </TextBlock> </Grid> </Navi gati onWi ndow.Content> </Page> Переходя между страницами приложения, обратите внимание на кнопки Back и Forward у верхнего края Navigationwindow. Navigationwindow ведет журнал посе- щенных страниц в двух стеках, соответствующих двум кнопкам. Каждый раз, когда программа использует метод Navigate для перехода к странице, текущая страница заносится в стек Back, а стек Forward очищается. При щелчке на кнопке Back текущая страница заносится в стек Forward, и происходит переход к стра- нице, извлеченной из стека Back. Если же щелкнуть на кнопке Forward, теку- щая страница заносится в стек Back, и происходит переход к странице из стека Forward. Переход вперед и назад также возможен на программном уровне, как будет показано позднее в этой главе. В начале файла РадеЗ.хат! определяется элемент TextBox. Введите в нем ка- кой-нибудь текст. Если снова вернуться к РадеЗ.хат! с использованием гипер- ссылок, поле TextBox оказывается пустым. Но если перейти к РадеЗ.хат! при по- мощи кнопок Back и Forward, введенный текст находится на месте. При использовании Navigationwindow все содержимое окна изменяется с каж- дым переходом. Возможно, вы захотите выделить под навигацию только часть окна или выполнять независимую навигацию в нескольких частях окна; в таких случаях следует использовать объект Frame. Проект FrameNavigationDemo содер- жит два файла FrameNavigationDemoApp.xaml и FrameNavigationWindow.xaml, а также содержит ссылки на четыре файла страниц из проекта NavigationDemo. Посколь- ку класс Раде2 содержит пространство имен Petzold.NavigationDemo, файл опреде-
Окна, страницы и перемещение 593 ления приложения FrameApplicationDemoApp.xaml определяет то же пространство имен с целью упрощения интеграции с более ранним кодом. FrameNavigationDemoApp.xaml <! - - ========================================================= FrameNavigationDemoApp.xaml (с) 2006 by Charles Petzold Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Class="Petzold.Navi gati onDemo.MyApp" StartupUri="FrameNavigationDemoWindow.xaml" /> Атрибуты StartupUri указывают на следующий файл FrameApplicationDemoWin- dow.xaml. FrameNavigationDemoWindow.xaml < i _ _ ============================================================ FrameNavigationDemoWindow.xaml (c) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Title="Frame Navigation Demo" FontSize="24"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Frame Grid.Row="0" Background="Aqua" Source="Pagel.xaml" /> <Frame Grid.Row="l" Background="Pink" Source="Page2.xaml" /> </Grid> </Window> Корневым элементом файла является элемент Window, но окно содержит два элемента Frame. Им назначены разные цвета, чтобы их было легче различить, и они ссылаются на разные файлы Раде. Откомпилируйте и запустите програм- му; вы увидите, что навигация между страницами в двух элементах Frame проис- ходит совершенно независимо. Пришло время вернуться к проекту YellowPad и создать для него справоч- ную систему. Проект YellowPad содержит каталог с именем Help, который со- держит десять XAML-файлов и три PNG-файла с графикой диалоговых окон программа. XAML-файлы весьма похожи друг на друга. Все они имеют корне- вой элемент Раде с элементом FlowDocument внутри элемента FlowDocumentReader, содержащим текст справки и, возможно, — ссылку на одно из изображений или другой справочный файл. Далее приводится типичный файл из каталога Help.
594 Глава 22. Окна, страницы и перемещение EraserToolDialog.xaml <।_ _ =================================================== EraserToolDialog.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" T1tle="The Eraser Tool D1alog"> <FlowDocumentReader V1ew1ngMode="Scrol1"> <FlowDocument> Paragraph TextAl1gnment="Center" FontS1ze="16pt"> The Eraser Tool Dialog </Paragraph> <Paragraph> The <Bold>Eraser Tool</Bold> dialog lets you change the dimensions and shape of the eraser. </Paragraph> <BlockUIConta1ner> <Image Source="EraserToolDialog.png" Stretch="None"/> </BlockUIConta1ner> <Paragraph> Use the <Bold>W1dth</Bold> and <Bold>He1ght</Bold> fields to specify the dimensions of the eraser In points (l/72<Run BaselIneAl1gnment="Superscr1pt">nd</Run> Inch). </Paragraph> <Paragraph> lise the <Bold>Rotat1on</Bold> field to specify a rotation of the eraser. The rotation only makes sense when the horizontal and vertical dimensions of the eraser are unequal. </Paragraph> </FlowDocument> </F1owDocumentReader> </Page> Файл YellowPadHelp.xaml содержит макет окна справки. Его корневым элементом является элемент Navigationwindow, содержащий элемент Grid с тремя столбцами. YellowPadHelp.xaml <।_ _ ================================================ YellowPadHelp.xaml (с) 2006 by Charles Petzold <Nav1gat1onW1ndow xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation"
Окна, страницы и перемещение 595 xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml" x:Class="Petzold.Yel1owPad.Yel1owPadHelp" T1tle="YellowPad Help" W1dth="800" He1ght="600" ShowInTaskbar="False" WIndowStartupLocatlon="CenterScreen"> <Na vigat1onWindow.Content> <Gr1d> <G r1d.ColumnDef1niti ons> <ColumnDef1n1t1on W1dth="25*" /> ColumnDefinition W1dth="Auto" /> ColumnDefinition W1dth="75*" /> </G r1d.ColumnDef1nitions> <TreeV1ew Name="tree" FontS1ze="10pt" SeiectedItemChanged="HelpOnSelectedItemChanged"> <TreeV1ewItem Header="Program Overview" Tag="Help/0verv1ew.xaml" /> <TreeV1ewItem Header="Explor1ng the Menus"> <TreeViewitem Header="The File Menu" Tag="Help/F11eMenu.xaml" /> <TreeV1ewItem Header="The Edit Menu" Tag="Help/Ed1tMenu.xaml" /> <TreeV1ewItem Header="The Stylus-Mode Menu" Tag="Help/StylusModeMenu.xaml" /> <TreeV1ewItem Header="The Eraser-Mode Menu" Tag="Help/EraserModeMenu.xaml" /> <TreeV1ewItem Header="The Tools Menu" Tag="Help/ToolsMenu.xaml"> <TreeViewItem Header="The Stylus Tool Dialog" Tag="Help/StylusToolDialog.xaml" /> <TreeVlewitem Header="The Eraser Tool Dialog" Tag="Help/EraserToolD1alog.xaml" /> </TreeV1ewItem> <TreeViewItem Header="The Help Menu" Tag^’Help/HelpMenu.xaml" /> </TreeV1ewItem> <TreeViewItem Header="Copyr1ght Information" Tag="Help/Copyr1ght.xaml" /> </TreeV1ew> CridSplItter Grid.Columnar W1dth="6" HorlzontalAl 1gnment="Center" Vert1calA11gnment="Stretch" /> <Frame Name="frame" Gr1d.Column="2" /> </Gr1d> </Navigat1onWindow.Content> </Nav1gat1onW1ndow>
596 Глава 22. Окна, страницы и перемещение Первый столбец Grid содержит элемент TreeView, выполняющий функции спи- ска содержимого. Я связал универсальное свойство Тад каждого элемента Tree- ViewItem с XAML-файлом, связанным с этим элементом. Справа от объекта Grid- Splitter располагается объект Frame с именем фрейма. Событию SelectedltemChanged объекта TreeView назначается обработчик, реализованный в файле программной логики YellowPadHelp.cs. YellowPadHelp.cs // // YellowPadHelp.cs (с) 2006 by Charles Petzold // using System; using System.Windows: using System.Windows.Controls: namespace Petzold.YellowPad { public partial class YellowPadHelp { public YellowPadHelpO { InitializeComponent(): // Выделение первого элемента в объекте TreeView // и передача ему фокуса (tree.ItemsEO] as TreeViewItem).IsSelected = true: tree.Focus 0: } void HelpOnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> args) { TreeViewItem item = args.NewValue as TreeViewItem; if (item.Tag == null) return: // Переход по свойству Tag выделенного элемента frame.Navigate(new Uri(item.Tag as string, UriKind.Relative)): } } } Каждый раз, когда пользователь выделяет один из элементов списка TreeView слева, обработчик события получает свойство Тад, преобразует его в Uri и пере- дает методу Navigate объекта Frame. В результате мы получаем простую и эле- гантную справочную систему. В главе 25 я продемонстрирую более общий под- ход к выводу справочной информации.
QKHaz страницы и перемещение 597 Классы Navigationwindow, Frame и Navigationservices поддерживают некоторые события, которые позволяют отслеживать ход навигации и прервать ее в случае необходимости. Разумеется, эти события важны при загрузке страниц по сети (а не тогда, когда все содержимое находится на жестком диске пользователя). Распространенную категорию навигационных приложений составляют мас- тера- Мастер (wizard) представляет собой последовательность страниц, зани- мающих одно и то же окно и предназначенных для накопления информации. Как правило, на каждой странице мастера присутствуют кнопки Previous и Next для возврата к предыдущей и перехода к следующей странице (кроме первой и последней страницы). Поскольку для выполнения навигации мастер использует собственные кноп- ки, мастер WPF, построенный на базе Navigationwindow или Frame, не должен отображать стандартные средства навигации (для Navigationwindow задайте Shows- Navigationlll значение false; для Frame задайте NavigationUIVisibility значение Hidden). Сама программа должна обеспечивать навигацию в обработчиках события Click кнопок Previous и Next. Несмотря на «ручную» обработку навигации, применение средств WPF значительно упрощает задачу построения мастеров — прежде все- го потому, что вам не приходится отслеживать перемещения пользователя по страницам. Крайне желательно, чтобы страницы мастера не теряли информацию между переходами. Нехорошо, если пользователь, вернувшийся к предыдущей страни- це кнопкой Previous, обнаружит пустую страницу, которую придется заполнять с самого начала. Все мы сталкивались с мастерами (и гораздо чаще — веб-страни- цами) с плохо реализованной навигацией. Не стоит подражать таким образцам. Я приведу относительно простой пример мастера, имитирующего работу компьютерной службы знакомств. Не пугайтесь 15 файлов с исходным кодом! Для каждой из 5 страниц мастера необходимы файлы с кодом XAML и С#. Одна из страниц содержит функцию вызова другой страницы, для которой так- же необходимы файлы XAML и С#. Окно, в котором все компоненты собира- ются воедино, также представлено файлами XAML и С#. Пятнадцатый файл проекта представлен ниже. В него включены открытые поля для хранения всей информации, накопленной мастером. Vitals.cs // И Vitals.cs (с) 2006 by Charles Petzold И using System; using System.Windows; using System.Windows.Controls; namespace Petzold.ComputerDatlngWIzard { public class Vitals { public string Name;
598 Глава 22. Окна, страницы и перемещение public string Home; public string Gender; public string FavorlteOS: public string Directory; public string MomsMaldenName; public string Pet: public string Income: public static RadioButton GetCheckedRad1oButton(GroupBox grpbox) { Panel pnl = grpbox.Content as Panel: If (pnl != null) foreach (UIEIement el In pnl.ChiIdren) { RadioButton radio = el as RadioButton; If (radio != null && (bool)radio.IsChecked) return radio; } } return null: } } } В этом файле собраны основные данные, накапливаемые мастером во вре- мя перемещений между страницами. Для простоты данные хранятся в стро- ковых переменных, но некоторые из них получают информацию из элементов RadioButton, сгруппированных на панели StackPanel в объекте GroupBox. По этой причине я включил статический метод, который упрощает получение установ- ленного переключателя из группы. За все время своего выполнения программа отображает всего одно неболь- шое окно, созданное на основе следующего XAML-файла. ComputerDatingWizard.xaml <।_ _ ==^==================================================== ComputerDatingWizard.xaml (с) 2006 by Charles Petzold <W1ndow xmlns="htIp://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http;//schemas.mlcrosoft.com/winfx/2006/xaml" x:Class="Petzold.ComputerDatlngWIzard.ComputerDatlngWIza rd" W1ndowStartupLocat1on="CenterScreen" T1tle="Computer Dating Wizard" W1dth="300" He1ght="300" Res1zeMode="NoRes1ze"> <Gr1d> <Gr1d.RowDef1n1t1ons> <RowDef1nition Helght="Auto" /> <RowDef1n1t1on He1ght="*" /> </Gr1d.RowDef1n1t1ons>
QKHaz страницы и перемещение 599 <TextBlock Gr1d.Row="0" Marg1n="12" FontS1ze="16" FontStyle="Italic" HorlzontalAl1gnment="Center"> Computer Dating Wizard </TextBlock> <Frame Gr1d.Row="l" Name="frame" Nav1gat1onllIV1s1b111ty="H1dden" Padd1ng="6" /> </Gr1d> </W1ndow> Окно имеет фиксированный размер, что характерно для мастеров. Клиентская область делится между текстом «Computer Dating Wizard» и объектом Frame, в котором отображаются разные страницы. Текстовая строка просто символизи- рует область окна, которая остается постоянной на все время существования мастера. Такая область не обязана располагаться наверху и содержать текст. Обратите внимание: свойству NavigationUIVisibility объекта Frame задано значение Hidden. Объект Frame не должен отображать собственные средства навигации, потому что навигация будет обеспечиваться кнопками на страницах мастера. Код C# этого класса содержит метод Main (конечно, я также мог использовать файл определения приложения) и конструктор, вызывающий InitializeComponent. ComputerDatingWizard.cs // // ComputerDat1ngW1zard.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; namespace Petzold.ComputerDatlngWIzard { public partial class ComputerDatlngWIzard { [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new ComputerDatlngWIzard()); } public ComputerDatlngWIzard() { Initial IzeComponentO; // Переход к странице с приветствием frame.Nav1gate(new WIzardPageO());
600 Глава 22. Окна, страницы и перемещение Конструктор завершается альтернативной формой метода Navigate, при вызо- ве которого используется объект вместо экземпляра Uri. Хотя аргумент Navigate определяется как object, в действительности он представляет корневой элемент дерева, поэтому он близко соответствует XAML-файлу, заданному в URL Тем не менее данная форма Navigate в некоторых ситуациях оказывается более гиб- кой. Вместо того чтобы сразу передавать вновь созданный объект Navigate, вы можете задать некоторые свойства объекта. А может быть, объект будет созда- ваться конструктором, которому передаются параметры. Я буду использовать последнее решение для передачи экземпляра класса Vitals между страницами мастера. Класс ComputerDatingWizard может вызывать Navigate непосредственно для эле- мента Frame, потому что элемент Frame является частью класса. У будущих стра- ниц прямой доступ к Frame отсутствует, поэтому для навигации им придется ис- пользовать объект Navigationservices. Класс WizardPageO содержит вводное сообщение с единственной кнопкой Begin. WizardPageO.xaml <I_ _ ============================================== WizardPageO.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Class="Petzold.ComputerDati ngWi zard.Wi zardPageO"> <Grid> <Gri d.RowDefi ni ti ons> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Gri d.RowDefi ni ti ons> <FlowDocumentScrollViewer Grid.Row="0" Margin="6" Vertical ScrollBarVisi bility="Hidden"> <FlowDocument FontSize="10pt"> <Paragraph> Welcome to the Computer Dating Wizard. This program probes the <Italic>Inner You</Italic> to match you with the mate of your dreams. </Paragraph> <Paragraph> To begin, click the Begin button. </Paragraph> </FlowDocument> </F1owDocumentScrol1 Vi ewer> <!-- Кнопка навигации в правом нижнем углу страницы --> <Grid Grid.Row="2">
Окна, страницы и перемещение 601 <G г1d.ColumnDef1n111 on s> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> </Gr1d.ColumnDef1ni ti ons> <Button Grid.Column="l" Click="BeginButtonOnClick" MinWidth="72" Margin=,,6" Content="Begin &gt;" /> </Gr1d> </Gr1d> </Page> Учтите, что объект Frame, в котором отображается страница, имеет фиксиро- ванный размер, определяемый общим размером окна, заданным в ComputerDating- Wizard.xaml, за вычетом размера объекта TextBlock, находящегося над Frame. Одна- ко навигационные кнопки, находящиеся на каждой странице, должны находить- ся у нижнего края клиентской области. У панели Grid в файле WizardPageO.xaml определяются три строки; первая и третья имеют размер Auto, а вторая занимает все оставшееся место. Первую строку занимает объект FlowDocumentScrollViewer, а третью — вторая панель Grid, определяемая в конце файла. Фактически вторая строка остается неиспользуемой и существует только для того, чтобы третья строка размещалась внизу. Аналогично у второго объекта Grid определяются два столбца: первый использует все оставшееся место, а второму назначается шири- на Auto. Первый столбец не используется, а кнопка Begin занимает второй стол- бец. В результате кнопка располагается у правого края окна. Я использовал этот прием для сохранения постоянной позиции кнопок отно- сительно окна. Файл программной логики класса WizardPageO содержит обработ- чик события для кнопки Begin. WizardPageO.cs // И WizardPageO.cs (с) 2006 by Charles Petzold //- using System: using System.Windows; using System.Windows.Controls; namespace Petzold.ComputerDatlngWizard { public partial class WizardPageO { public WizardPageO() { InitializeComponent(); } void BeginButtonOnClick(object sender. RoutedEventArgs args) { i f (Navi gat1onServi ce.CanGoForward) Navigationservice.GoForward();
602 Глава 22. Окна, страницы и перемещение else { Vitals vitals = new VltalsO; WizardPagel page = new WlzardPagel(vltals); Navigationservice.Navigate(page); } } } Пользователь может просматривать страницу WizardPageO в двух ситуациях — либо при запуске программы, либо в результате щелчка на кнопке Previous на следующей странице (WizardPagel.xaml). Так как программа использует метод Navigate для перемещения между страницами, список страниц сохраняется в жур- нале. В классах Navigationwindow, Frame и Navigationservices определяются два свойства, указывающие, содержит ли журнал запись для перехода назад к преды- дущей или перехода вперед к ранее посещенной странице. Эти свойства на- зываются CanGoBack и CanGoForward; кроме того, в классах Navigationwindow, Frame и Navigationservices присутствуют методы GoBack и GoForward для перехода к этим страницам. Если пользователь запустит мастера, щелкнет на кнопке Begin страницы Wiz- ardPageO.xaml, затем щелкнет на кнопке Previous страницы WizardPagel.xaml и сно- ва на кнопке Begin страницы WizardPageO.xaml, обработчик Click кнопки Begin бу- дет знать о произошедшем, потому что свойство CanGoForward будет равно true. Это позволит ему вызвать GoForward для перехода к WizardPagel.xaml. Преиму- щество методов GoBack и GoForward (когда они доступны) заключается в том, что страница уже создана, загружена и, возможно, изменена пользователем. При- менение этих методов сохраняет данные, введенные пользователем на странице. Если свойство CanGoForward ложно, значит, пользователь просматривает Wiz- ardPageO.xaml впервые. При щелчке на кнопке Begin класс WizardPageO создает но- вый объект типа Vitals. Этот объект будет существовать на протяжении всего жизненного цикла мастера. Затем обработчик создает объект типа WizardPagel, передавая объект Vitals его конструктору. Его выполнение завершается перехо- дом к следующей странице. Макет класса WizardPagel определяется следующим XAML-файлом. WizardPagel.xaml <।_ _ ============================================== WizardPagel.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentatlon" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml" x:Cl ass="Petzold.ComputerDatlngWIzard.WIzardPagel"> <Gr1d> <Gr1d.RowDef1n1t1ons> <RowDef1n1t1on He1ght="Auto" />
Окна, страницы и перемещение 603 <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Gri d.ColumnDefi ni ti ons> ColumnDefinition Width="50*" /> ColumnDefinition Width="50*" /> </Grid.ColumnDefi ni ti ons> <!-- Объект TextBox для ввода имени --> CtackPanel Orientation-"Horizontal” Grid.ColumnSpan="2" Margin="12"> <Label> Name: </Label> <TextBox Name="txtboxNarne" Width="200" /> </StackPanel> <!-- Объект GroupBox для ввода адреса --> CroupBox Grid.Row="l" Grid.Column="0" Name="grpboxHome" Header="Home" Margin-"12"> CtackPanel > <RadioButton Content="House" Margin="6" IsChecked="True" /> <RadioButton Content="Apartment" Margin="6" /> <RadioButton Content="Cave" Margin="6" /> </StackPanel> </GroupBox> <!-- Объект GroupBox для ввода пола --> CroupBox Grid.Row-’T’ Grid.Column-"Г Name="grpboxGender" Header="Gender" Margin-”12"> CtackPanel > <RadioButton Content-"Male" Margin=”6" IsChecked-"Tiue" /> <RadioButton Content=”Female" Margin="6" /> <RadioButton Content="Flexible" Margin="6” /> </StackPanel> </GroupBox> <!-- Кнопки перехода в правом нижнем углу страницы --> <Grid Grid.Row="3" Grid.ColumnSpan=”2"^ <Gri d.ColumnDefi ni ti ons> ColumnDefinition Width="*" /> ColumnDefinition Width="Auto" /> ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Button Grid.Column="l" Click="PreviousButtonOnClick"
604 Глава 22. Окна, страницы и перемещение MinWidth="72" Margin="6" Content="&lt; Previous" /> <Button Grid.Column="2" Click="NextButtonOnClick" MinWidth="72" Margin="6" Content="Next &gt;" /> </Grid> </Gr1d> </Page> Страница содержит элемент TextBox и два элемента GroupBox, каждый из ко- торых содержит три элемента RadioButton. Две кнопки у нижнего края помечены надписями Previous и Next. Файл программной логики класса WizardPagel содержит конструктор с одним параметром. Объект Vitals, переданный конструктору, сохраняется в поле класса. Конструктор также должен вызвать метод InitializeComponent. WizardPagel.cs // // WizardPagel.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; namespace Petzold.ComputerDatlngWizard { public partial class WizardPagel: Page { Vitals vitals; // Конструкторы public WizardPageKVitals vitals) { InitializeComponent(); this.vitals = vitals; } // Обработчики событий для кнопок Previous и Back void PreviousButtonOnClick(object sender, RoutedEventArgs args) { Navigationservice.GoBack(); } void NextButtonOnClick(object sender, RoutedEventArgs args) { vitals.Name = txtboxName.Text; vitals.Home = Vitals.GetCheckedRadioButton(grpboxHome).Content as string; vitals.Gender =
Окна, страницы и перемещение 605 Vitals.GetCheckedRadloButton(grpboxGender).Content as string; If (Navigationservice.CanGoForward) Navigationservice.GoForward(): else { W1zardPage2 page = new W1zardPage2(v1tals); Navigationservice.Navigate(page); } } } } Работа кнопки Previous обеспечивается простым вызовом GoBack. Обработ- чик может быть абсолютно уверен, что вызов GoBack вернет страницу Wizard- PageO.xaml, потому что только по этому маршруту пользователь мог перейти к WizardPagel.xaml. Обработчик кнопки Next получает данные, введенные на странице, и сохра- няет их в объекте Vitals. Затем он переходит к классу WizardPage2 тем же спосо- бом, которым WizardPageO перешел к WizardPagel. Однако следует учесть, что об- работчик может принять некоторые решения на основании введенных данных. Возможно, мастер может использовать разные пути выполнения в зависимости от того, какой переключатель будет выбран пользователем в одной из групп. Класс WizardPage2, как обычно, начинается с XAML-файла. WizardPageZ.xaml < । _ _ ============================================== WizardPage2.xaml (с) 2006 by Charles Petzold <Page xml ns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentatlon" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml" x:Class="Petzold.ComputerDatlngWIzard.WIzardPage2"> <Gr1d> <G r1d.RowDefinitlons> <RowDef1n1t1on He1ght="Auto" /> <RowDef1nition Helght="Auto" /> <RowDef1n1t1on He1ght="Auto" /> <RowDef1nition Helght="Auto" /> <RowDef1n1t1on He1ght="Auto" /> <RowDef1n1t1on He1ght="*" /> <RowDef1nition He1ght="Auto" /> </Gr1d.RowDefinitlons> <!-- Объект TextBox для ввода операционной системы --> <TextBlock Gr1d.Row="0" Marg1n="0, 12. 0, 0"> Favorite operating system: </TextBlock> <TextBox Gr1d.Row=”l" Name="txtboxFavor1teOS" Text="M1crosoft Windows Vista, of course!" />
606 Глава 22. Окна, страницы и перемещение <!-- Объект TextBox для ввода каталога --> <TextBlock Grid.Row="2" Margin="O, 12. 0. 0"> Favorite disk directory: </TextBlock> <TextBox Grid.Row="3" Nanie="txtboxFavoriteDir" Text="C:\"/> <Button Grid.Row="4" Click="BrowseButtonOnCl ick" Hori zontalAli gnment="Right" MinWidth="72" Margin="0, 2. 0. 0" Content="Browse..." /> <!-- Кнопки перехода в правом нижнем углу страницы --> <Grid Grid.Row="6"> <Gri d.ColumnDefi ni ti ons> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Gri d.ColumnDefi ni ti ons> <Button Grid.Column='T' Click="PreviousButtonOnClick" MinWidth="72" Margin="6" Content="&lt: Previous" /> <Button Grid.Column="2" Click="NextButtonOnClick" MinWidth="72" Margin="6" Content="Next &gt;" /> </Grid> </Gr1d> </Page> Эта страница выглядит несколько иначе. Она содержит обычные кнопки Previous и Next, но в ней также имеется поле TextBox для ввода каталога и кнопка Browse. Предполагается, что кнопка Browse открывает окно или страницу с эле- ментом TreeView, и пользователь выбирает нужный каталог вместо того, чтобы вводить его вручную. Однако щелкать на кнопке Browse не обязательно, поэтому она может рассматриваться как отклонение от «основного пути» мастера. Кнопка Browse может работать в одном из двух режимов. Во-первых, она может вызвать модальное диалоговое окно, которое выводится отдельно поверх окна Computer- DatingWizard. Во-вторых, новое окно может быть реализовано в виде класса Раде, который используется для перехода как обычная страница мастера. Я выбрал второй вариант. Окно или страница, вызываемое кнопкой Browse, должно возвращать значе- ние. В нашем случае на эту роль подойдет объект Directoryinfo. Значение должно возвращаться коду WizardPage2, чтобы класс WizardPage2 мог заполнить TextBox путем к выбранному каталогу. При использовании диалогового окна вы може- те определить для этой информации свойство и сообщать о способе закрытия окна (ОК или Cancel) установкой свойства DialogResult. Конечно, вы также мо- жете определить свойство Directoryinfo в классе Раде, но это несколько усложнит возврат данных страницей.
ОкНа, страницы и перемещение 607 «Отклонение» необходимо исключить из журнала посещений, чтобы кнопка Next на странице WizardPage2.xaml не вызывала метод GoForward, который бы пере- водил пользователя к странице с элементом TreeView для выбора каталога. Для упрощения этой задачи создан класс PageFunction, производный от Раде. Он пред- назначен для отображения страницы, возвращающей значение. Класс является шаблонным: тип данных, возвращаемых классом, задается в определении Page- Function. Кроме того, PageFunction изменяет содержимое журнала, чтобы предотвра- тить случайные повторения отклонений от «основного пути» выполнения мастера. Класс, который я определил производным от PageFunction для отображения TreeView, называется DirectoryPage; он возвращает объект типа Directoryinfo. Далее приводится содержимое файла DirectoryPage.xaml. DirectoryPage.xaml <! - - ================================================ DirectoryPage.xaml (с) 2006 by Charles Petzold <PageFunction xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:1o="clr-namespace:System.10:assembly=mscor1lb" xml ns:tree="clr-namespace:Petzold.RecurseDirectoriesIncremenially" x:Class="Petzold.ComputerDatlngWi zard.Di rectoryPage" x:TypeArguments="i о:Di rectory Info"> <Grid> <Gri d.RowDefi ni ti ons> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Gri d.RowDefi ni 11ons> <TextBlock Grid.Row="0" FontSize="16" FontStyle="Italic" Horizontal Al1gnment="Center"> Computer Dating Wizard </TextBlock> <tree:DirectoryTreeView x:Name="treevue" Grid.Row="l" /> <!-- Кнопки перехода в правом нижнем углу страницы --> <Grid Grid.Row="2"> <G г1d.ColumnDef1ni ti ons> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> <ColumnDef1 nition Width="Auto" /> </Grid.ColumnDefinitions> <Button Grid.Column="l" Click="CancelButtonOnClick" IsCancel="True" MinWidth="60" Margin="6"> Cancel </Button>
608 Глава 22. Окна, страницы и перемещение <Button Grid.Column="2" Name="btnOk" Click="OkButtonOnClick" IsEnabled="False" IsDefault="True" MinWidth="60" Margin="6"> OK </Button> </Gr1d> </Grid> </PageFunction> Обратите внимание: атрибут x:Class, как обычно, определяет имя класса, но из-за обобщенного характера класса PageFunction корневой элемент также дол- жен содержать атрибут x:TypeArguments для обозначения типа (в данном случае Directoryinfo). Класс Directoryinfo определяется в пространстве имен System.10. Другое объявление пространства имен связывает префикс tree с пространством имен проекта RecurseDirectoriesIncrementally из главы 16. Из этого проекта берется класс DirectoryTreeView, занимающий основную часть страницы. Макет PageFunction завершают кнопки Cancel и ОК. DirectoryPage.cs // // DirectoryPage.cs (с) 2006 by Charles Petzold // using Petzold.RecurseDirectoriesIncrementally; using System; using System.ID; using System.Windows; using System.Windows.Controls; using System.Windows.Navigation; namespace Petzold.ComputerDati ngWi zard { public partial class Di rectoryPage : PageFunction<DirectoryInfo> { // Конструктор public DirectoryPage() { Initial izeComponentO; treevue.SeiectedltemChanged += TreeViewOnSelectedltemChanged; } // Обработчик события для снятия блокировки с кнопки ОК void TreeViewOnSelectedItemChanged(object sender. RoutedPropertyChangedEventArgs<object> args) { btnOk.IsEnabled = args.NewValue != null; } // Обработчики событий для кнопок Cancel и OK void CancelButtonOnClick(object sender, RoutedEventArgs args)
Окна, страницы и перемещение 609 { OnReturn(new ReturnEventArgs<DirectoryInfo>()); } void OkButtonOnCl1ck(object sender, RoutedEventArqs args) { Di rectoryinfo dirinfo = (treevue.Selectedltem as DirectoryTreeViewItem).Directoryinfo; OnReturn(new ReturnEventArgs<DirectoryInfo>(dirinfo)); } } Конструктор устанавливает обработчик события SelectionChanged к элементу DirectoryTreeView, чтобы кнопка ОК становилась доступной только после выбора каталога. Также конструктор можно было определить с аргументом, чтобы пото- мок PageFunction мог инициализировать себя на основании информации, полу- ченной от вызывающей страницы. Класс, производный от PageFunction, завершается вызовом OnReturn с аргумен- том типа ReturnEventArgs — еще одного шаблонного класса, для которого необхо- дима информация о типе объекта, возвращаемого потомком PageFunction. Конст- руктору ReturnEventArgs передается сам объект. Обратите внимание: обработчик события кнопки Cancel не передает аргумент конструктору ReturnEventArgs. При завершении PageFunction запись автоматически удаляется из журнала, на что указывает истинное значение свойства RemoveFromJournal. Класс WizardPage2 содержит обработчик события Click для кнопки Browse, ко- торый осуществляет переход к странице DirectoryPage, как видно из файла про- граммной логики WizardPage2.cs. WizardPage2.cs //--------------------------------------------- // WizardPage2.cs (с) 2006 by Charles Petzold //--------------------------------------------- using System; using System.ID; using System.Windows: using System.Windows.Controls: using System.Windows.Navigation: namespace Petzold.ComputerDatlngWi zard public partial class WizardPage2 { Vitals vitals: // Конструктор public WizardPage2(Vitals vitals) { Initial!zeComponent(); this.vitals = vitals: }
610 Глава 22. Окна, страницы и перемещение // Обработчики событий для необязательной кнопки Browse void BrowseButtonOnC11ck(object sender. RoutedEventArgs args) { DlrectoryPage page = new DirectoryPage(); page.Return += DirPageOnReturn; Navigationservice.Navigate(page); } void D1rPageOnReturn(object sender, ReturnEventArgs<D1rectoryInfo> args) { If (args.Result != null) txtboxFavorlteDlr.Text = args.Result.FullName: } // Обработчики событий для кнопок Previous и Back void Prev1ousButtonOnC11ck(object sender, RoutedEventArgs args) { Navigationservice.GoBack(); } void NextButtonOnC11ck(object sender, RoutedEventArgs args) { vitals.FavorlteOS = txtboxFavorlteOS.Text; vitals.Directory = txtboxFavorlteDlr.Text; If (Navigat1onServIce.CanGoForward) Navigationservice.GoForward(); else { W1zardPage3 page = new W1zardPage3(v1tals); Navi gat1onServIce.Navigate(page); } } } } Когда пользователь щелкает на кнопке Browse, обработчик события создает объект DirectoryPage, устанавливает обработчик для события Return и переходит к этой странице. Обработчик события Return получает значение, возвращаемое PageFunction, из свойства Result объекта ReturnEventArgs. Для класса DirectoryPage свойство Result представляет собой объект типа Directoryinfo. Если пользователь закрыл страницу кнопкой Cancel, свойство равно null; в противном случае оно обозначает каталог, выбранный пользователем. Обработчик события переносит имя каталога в TextBox. Оставшаяся часть WizardPage2.cs выглядит вполне обычно. Обработчик кноп- ки Next сохраняет ввод пользователя и переходит к странице WizardPage3.xaml. Страница WizardPage3.xaml очень похожа на WizardPagel.xaml, не считая того, что кнопка Next заменена кнопкой Finish — это последняя страница ввода дан- ных. Как обычно, расположение элементов и кнопок описано в XAML-файле.
Окна, страницы и перемещение 611 WizardPage3.xaml WizardPage3.xaml (с) 2006 by Charles Petzold <Page xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentat1on" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml" x:Class="Petzold.ComputerDatlngWizard.WIzardPage3"> <Grid> < Gr1d.RowDefi ni 11ons> <RowDefinition Height="Auto" /> <RowDefin1tion Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Helght="Auto" /> </Grid.RowDefinitions> < Gr1d.ColumnDef1ni ti ons> <ColumnDef1nition Width="50*" /> <ColumnDef1nition W1dth="50*" /> </Gr1d.ColumnDef1ni ti ons> < !-- Объект TextBox для ввода фамилии матери --> < StackPanel Orientation="HorizontaT Grid.ColumnSpan="2" Margin="12"> <Label> Mother's Maiden Name: </Label> <TextBox Name="txtboxMom" W1dth="100" /> </StackPanel> < !-- Объект GroupBox для сведений о домашнем животном --> < GroupBox Grid.Row="l" Grid.Column="0" Name="grpboxPet" Header="Favorite Pet" Margin="12"> <StackPanel> <RadioButton Content="Dog" Margin="6" IsChecked="True" /> <RadioButton Content="Cat" Margin="6" /> <RadioButton Content="Iguana" Margin="6" /> </StackPanel> </GroupBox> < !-- Объект GroupBox для сведений о доходе --> <GroupBox Grid.Row="l" Grid.Column="l" Name="grpboxlncome" Header="Income Level" Margin="12"> <StackPanel> <RadioButton Content="R1ch" Margin="6" IsChecked="True" /> <RadioButton Content="So-so" Margin="6" /> <RadioButton Content="Freelancer" Margin="6" /> </StackPanel> </GroupBox>
612 Глава 22. Окна, страницы и перемещение <!-- Кнопки перехода в правом нижнем углу страницы --> <Grid Grid.Row="3" Grid.ColumnSpan="2"> <Gr1d.ColumnDefi ni ti ons> <ColumnDef1nition Width="*" /> <ColuninDef1nition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Gr1d.ColumnDef1ni ti ons> <Button Grid.Columnar Cl1ck="PreviousButton0nClick" MinWidth="72" Margin="6" Content="&lt: Previous" /> <Button Grid.Column="2" Click="FinishButton0nC11ck" MinWidth="72" Margin="6" Content="Finish &gt:” /> </Grid> </Grid> </Page> Далее приводится файл программной логики для страницы WizardPage3. WizardPage3.cs // // WizardPage3.cs (с) 2006 by Charles Petzold // using System; using System.Windows; using System.Windows.Controls; namespace Petzold.ComputerDatlngWi zard { public partial class WizardPage3: Page { Vitals vitals; // Конструктор public WizardPage3(Vitals vitals) { InitializeComponentO; this.vitals = vitals; } // Обработчики событий для кнопок Previous и Finish void PreviousButtonOnClick(object sender, RoutedEventArgs args) { Navigationservice.GoBack(); } void FinishButton0nC11ck(object sender. RoutedEventArgs args) { // Сохранение информации с этой страницы vitals.MomsMaidenName = txtboxMom.Text;
Окна, страницы и перемещение 613 vitals.Pet = Vitals.GetCheckedRadioButton(grpboxPet).Content as string; vitals.Income = Vitals.GetCheckedRadioButton(grpboxIncome).Content as string; // Последняя страница всегда создается заново W1zardPage4 page = new W1zardPage4(v1tals); Navigationservice.Navi gate(page): } } Как обычно, обработчик события кнопки Next (в этом окне она называется Finish) сохраняет данные, введенные на странице, в объекте Vitals. Но вместо того, чтобы определять, можно ли вызвать GoForward для перехода к последней стра- нице, обработчик всегда создает заново объект типа WizardPage4. Вскоре вы пой- мете, почему это происходит. Последняя страница на основе класса WizardPage4 просто выводит всю инфор- мацию, собранную в мастере. Я выбрал относительно простой способ отображе- ния текстовой информации с определением нескольких объектов Run, входящих в один элемент TextBlock. WizardPage4.xaml < । _ _ ============================================== WizardPage4.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.m1crosoft.com/w1nfx/2006/xaml/presentation" xml ns:x=”http://schemas.mlcrosoft.com/winfx/2006/xaml" x:Class="Petzold.ComputerDatlngWIzard.WIzardPage4"> <Gr1d> <G r1d.RowDefinitions> <RowDef1n1t1on He1ght="Auto" /> <RowDefinition Height^"*" /> <RowDef1n1t1on He1ght="Auto" /> </G r1d.RowDefinitions> <TextBlock Grid.Row=”0”> <L1neBreak /> <Run Text="Name; " /> <Run Name="runName" /> <L1neBreak /> <Run Text="Home: " /> <Run Name="runHome" /> <L1neBreak /> <Run Text="Gender: " /> <Run Name="runGender" /> <L1neBreak /> <Run Text="Favor1te OS: " /> <Run Name="runOS" />
614 Глава 22. Окна, страницы и перемещение <L1neBreak /> <Run Text="Favorite Directory: " /> <Run Name="runD1rectory" /> <L1neBreak /> <Run Text="Mother's Malden Name: " /> <Run Name="runMomsMaidenName" /> <LineBreak /> <Run Text="Favorite Pet: " /> <Run Name="runPet" /> <L1neBreak /> <Run Text="Income Level: " /> <Run Name="runlncome" /> </TextBlock> <!-- Кнопки перехода в правом нижнем углу страницы --> <Grid Grid.Row="2"> <Gr1d.ColumnDef1ni ti ons> <ColumnDefinition W1dth="*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition W1dth="Auto" /> </Grid.ColumnDefi ni ti ons> <Button Grid.Column='T' C11ck="PreviousButton0nClick" M1nW1dth="72" Margin="6" Content="&lt: Previous" /> <Button Grid.Column="2" Click="SubmitButtonOnClick" MinWidth="72" Margin="6" Content="Submit!" /> </Grid> </Grid> </Page> На этот раз две кнопки помечены надписями Previous и Submit. Пользователь может вернуться назад и изменить какие-нибудь данные после просмотра. Файл программной логики завершает класс WizardPage4. WizardPage4.cs //-------------------------------------------- // WizardPage4.cs (с) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Windows; using System.Windows.Controls; namespace Petzold.ComputerDatlngWizard { public partial class WizardPage4: Page { // Конструктор public WizardPage4(Vitals vitals)
Окна, страницы и перемещение 615 { InitializeComponent(); // Задание текста на странице runName.Text = vitals.Name; runHome.Text = vitals.Home; runGender.Text = vitals.Gender: runOS.Text = vitals.FavoriteOS; runDirectory.Text = vitals.Di rectory; runMomsMaidenName.Text = vitals.MomsMaidenName; runPet.Text = vitals.Pet; run Income.Text = vitals.Income; } // Обработчики событий для кнопок Previous и Submit void PreviousButtonOnClick(object sender, RoutedEventArgs args) { Navigationservice.GoBack(); ) void SubmitButtonOnClick(object sender, RoutedEventArgs args) { MessageBox.Show("Thank you!\n\nYou will be contacted by email " + "in four to six months.", Appli cati on.Current.Mai nWi ndow.Title, MessageBoxButton.OK, MessageBoxImage.Exclamation); Application.Current.Shutdown(): } } } Как видно из листинга, конструктор класса WizardPage4 используется для за- полнения всех объектов Run на странице итоговым содержимым Vitals. Именно по этой причине класс WizardPage3 создает заново объект WizardPage4 при каждом переходе. Если пользователь решит щелкнуть на кнопке Previous и что-нибудь изменить, объект WizardPage4 должен вывести новую информацию, а самым удобным местом для этого является конструктор (при желании вы можете со- здавать класс Раде только один раз, а затем задавать информацию в обработчике события Loaded, вызываемом при каждом отображении страницы). Хотя создание мастера потребует написания немалого объема программного кода в дополнение к XAML-файлам, некоторые навигационные приложения во- обще не требуют программирования. В подкаталоге BookReader каталога приме- ров главы 22 представлена коллекция XAML-файлов с несколькими абзацами из книг Льюиса Кэрролла. Запустите файл BookReaderPage.xaml, показанный ниже. BookReaderPage.xaml < । _ _ =================:==;=:= BookReaderPage.xaml
616 Глава 22. Окна, страницы и перемещение <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" WindowTitle="Book Reader"> <Grid> <Gri d.RowDefi ni ti ons> <RowDefinition Height="10*" /> <RowDefinition Height="Auto" /> <RowDefinition Height="90*" /> </Grid.RowDefinitions> <!-- Объект Frame для списка книг --> <Frame Grid.Row="0" Source="BookList.xaml" /> <GridSplitter Grid.Row="l" Height="6” HorizontalAlignment="Stretch" VerticalAlignment="Center" /> <Grid Grid.Row="2"> <Gri d.ColumnDefi ni ti ons> ColumnDefinition Width="25*" /> ColumnDefinition Width=”Auto" /> ColumnDefinition Width="75*" /> < /Gri d.ColumnDefi n i ti ons> < !-- Объект Frame для оглавления --> <Frame Grid.Column="0" Name="frameContents" /> CridSplitter Grid.Columnar Width="6" HorizontalAlignment="Center" VerticalAlignment="Stretch" /> < !-- Объект Frame для текста --> < Frame Grid.Column="2" Name="frameChapter" /> </Grid> </Grid> </Page> При запуске файла страница отображается в Internet Explorer. Две панели Grid делят страницу на три области, разделенных вешками. Каждую область занима- ет элемент Frame: слева находится framecontents, а справа framechapter. У верхне- го элемента Frame свойству Source задано значение BookList.xaml. BookList.xaml <।_ _ =============== BookList.xaml <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <WrapPanel TextBlock.FontSize="10pt"> <TextBlock Margin="12"> <Hyperlink NavigateUri="Al iceInWonderland.xaml" Ta rgetName="framecontents"> <Italic>Alice's Adventures in Wonderland</Italic> by Lewis Carroll </Hyperlink>
Окна, страницы и перемещение 617 </TextBlock> <TextBlock Margin=’T2"> <Hyper11nk Navi gateUri="ThroughTheLookingGlass.xaml" TargetName=”framecontents”> <Italic>Through the Looking-Glass</Italic> by Lewis Carroll </Hyperlink> </TextBlock> <TextBlock Margin=’T2"> </TextBlock> </WrapPanel> </Page> Два элемента Hyperlink содержат не только атрибуты NavigateUri со ссылками XAML-файлы, но и атрибуты TargetName, ссылающиеся на элемент Frame в ле- вой части страницы BookList.xaml. Так элемент Hyperlink может разместить стра- ницу XAML в другом фрейме. Файлы AlicelnWonderland.xaml и ThroughTheLookingGlass.xaml очень похожи. Вот как выглядит первый из них. AlicelnWonderland.xaml <! - - ======================== Al icelnWonderland.xaml <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Title="Alice's Adventures in Wonderland"> <StackPanel TextBlock.FontSize="10pt"> <TextBlock Margin="12 12 12 6"> <Hyperlink NavigateUri="AliceChapterOl.xaml" TargetName="frameChapter"> Chapter I </Hyperlink> </TextBlock> <TextBlock Margin="12 6 12 6"> <Hyper1i nk Navi gateUri="Ali ceChapterO2.xaml" TargetName="frameChapter"> Chapter II </Hyperlink> </TextBlock> <TextBlock Margin="12 6 12 6"> <Hyperlink NavigateUri="AliceChapterO3.xaml" TargetName="frameChapter"> Chapter III </Hyperlink>
618 Глава 22. Окна, страницы и перемещение </TextBlock> <TextBlock Margin="12 6 12 6"> </TextBlock> </StackPanel> </Page> Далее приведена другая страница с несколькими элементами Hyperlink. Атри- буты NavigateUri ссылаются на отдельные главы, а атрибут TargetName указывает, что они должны отображаться во фрейме справа от исходной страницы. AliceChapterOl.xaml Al 1 ceChapterOl.xaml <Page xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" Title="I. Down the Rabbit-Hole"> <F1owDocumentReader> <FlowDocument> Paragraph TextAlignment="Center" FontSize="16pt"> Chapter I </Paragraph> Paragraph TextAlignment="Center" FontSize="16pt"> Down the Rabbit-Hole </Paragraph> Paragraph Textlndent="24"> Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do: once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it, &#x201C;and what is the use of a book,&#x201D; thought Alice, &#x201C:without pictures or conversations?&#x201D; </Paragraph> Paragraph Textlndent="24"> So she was considering, in her own mind (as well as she could, for the hot day made her feel very sleepy and stupid), whether the pleasure of making a daisy-chain would be worth the trouble of getting up and picking the daisies, when suddenly a White Rabbit with pink eyes ran close by her. </Paragraph> Paragraph Textlndent="24"> </Paragraph> </FlowDocument> </FlowDocumentReader> </Page>
Окна, страницы и перемещение 619 Если щелкнуть на названии книги в верхнем фрейме, содержимое книги ото- бражается слева. Если щелкнуть на названии главы в левом фрейме, ее содер- жимое отображается в правом фрейме. В подборку XAML-файлов также включен пример фрагментной навигации, напоминающей закладки HTML. В первую главу «Алисы в Зазеркалье» входит знаменитое стихотворение про Бармаглота. Его название оформлено отдельным абзацем, который выглядит в файле LookingGlassChapterOl.xaml следующим образом: Paragraph ... Name="Jabberwocky"> Jabberwocky </Paragraph> Присутствие атрибута Name позволяет приложению выполнить переход к дан- ному элементу страницы. В список глав ThroughTheLookingGlass.xaml также вклю- чена отдельная ссылка Jabberwocky, а для перехода к стихотворению имя файла отделяется от текста атрибута Name знаком #: <Нурег11nk NavigateUri=’lookingGlassChapterOl.xaml#Jabberwocky" TargetName="frameChapter"> "Jabberwocky" </Hyperlink> Существуют и другие элементы, часто применяемые при страничной органи- зации данных. Элемент TabControl, производный от Selector, содержит коллекцию элементов-вкладок Tabitem. Класс Tabitem является производным от Headered- ContentControl, а содержимое каждого объекта Tabitem занимает одну и ту же об- ласть страницы. Свойство Header определяет содержимое корешка вкладки; поль- зователь выбирает объект Tabitem, щелкая на корешке. По умолчанию вкладки расположены по горизонтали наверху, но это поведение можно изменить при помощи свойства TabStripPlacement. (За примером использования TabControl об- ращайтесь к программе TextGeometryDemo из главы 28.) Другой полезный элемент такого рода — Expander — тоже является производ- ным от HeaderedContentControl. Щелчок на заголовке Expander переводит его со- держимое из видимого состояния в свернутое, и наоборот. Нередко в содержи- мом присутствуют гиперссылки. Хотя запуск файла BookReaderPage.xaml позволяет просмотреть различные кни- ги и главы, входящие в проект, такое решение работает только благодаря тому, что все файлы находятся в одном каталоге и этот каталог связывается с Internet Explorer при запуске файла BookReaderPage.xaml. Конечно, все XAML-файлы мож- но упаковать в одном исполняемом файле. Рассматриваемые нами файлы входят в проект BookReader, который также содержит файл определения приложения BookReaderApp.xaml. BookReaderApp.xaml <! -. ================================================ BookReaderApp.xaml (с) 2006 by Charles Petzold Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" StartupUri="BookReaderPage.xaml" />
620 Глава 22. Окна, страницы и перемещение Этот файл просто запускает BookReaderPage.xaml — тот же файл, который мы запускали вручную из Проводника Windows. Проект BookReader является обычным проектом приложения WPF, но содер- жит только XAML-файлы. При построении проекта создается файл BookReader.exe, при запуске которого открывается объект Navigationwindow с XAML-файлами. Откуда он взялся? Ни один из XAML-файлов не содержит ссылок на Navigation- Window. Оказывается, когда файл определения приложения содержит атрибут StartupUri, ссылающийся на XAML-файл с корневым элементом Раде, для разме- щения страницы автоматически создается объект Navigationwindow. Итак, мы рассмотрели варианты с выполнением BookReaderPage.xaml в Internet Explorer посредством простого запуска XAML-файла из Проводника, а также компиляцию XAML-файлов в исполняемый файл BookReader.exe, который авто- матически создает объект Navigationwindow. Существует и третий вариант — фор- мат .xbap (XAML Browser Application). Проще всего создать его в Visual Studio. В проектах главы 22 я воспользовался Visual Studio для создания проекта XAML Browser Application с именем BookReader2. Затем я удалил файлы MyApp.xaml, MyApp.cs, Pagel.xaml и Pagel.cs, созданные Visual Studio, и включил в проект ссылки на XAML-файлы BookReader, не забыв пометить BookReaderApp.xaml как файл определения приложения. Проекты BookReader и BookReader2 состоят из одинаковых XAML-файлов с исходным кодом, но при построении проекта BookReader2 Visual Studio созда- ет файл BookReader2.xbap. При запуске этого файла из Visual Studio управление передается программе PresentationHost.exe — той, благодаря которой Internet Explorer обеспечивает отображение автономных XAML-файлов. Internet Explorer также может отображать приложения .xbap; результат выглядит так же, как при отображении отдельных XAML-файлов. Объект Navigationwindow, создаваемый BookReader.exe, отсутствует. Хотя для запуска приложения используется файл BookReader2.xbap, наряду с ним должны присутствовать еще два файла: BookReader2.exe и BookReader2.exe. manifest (впрочем, файл с расширением .ехе не является автономным исполняе- мым файлом). Поскольку приложения .xbap работают под управлением веб-браузера, они подчиняются определенным ограничениям. Такие программы не могут созда- вать объекты типа Window или Navigationwindow. Фактически они структури- руются на базе объектов Раде. При необходимости они переходят от страницы к странице посредством навигации. Приложения .xbap могут иметь меню, но не могут создавать диалоговые окна или объекты типа Popup. Такие программы ра- ботают с разрешениями безопасности, назначенными для зоны Интернета, и не могут использовать файловую систему. С другой стороны, такие приложения очень легко запускаются с веб-сайта. Чтобы установить приложение .xbap на сайте, вызовите свойства проекта в Visual Studio, щелкните на вкладке Publish слева и введите в поле Publishing Location FTP-адрес, обычно используемый для копирования файлов на сайт. Щелкните на кнопке Publish Now. Visual Studio в процессе копирования файлов на сайт запра- шивает имя пользователя и пароль FTP. Теперь в файл .xbap можно включить ссылку на веб-страницу. Если на компьютере пользователя установлена инфра-
Окна, страницы и перемещение 621 структура .NET Framework 3.0, щелчок на ссылке загрузит и запустит приложе- ние, не требуя специального разрешения со стороны пользователя. Постарайтесь как можно раньше определить, будет ли конкретная программа оформлена в виде полноценного приложения Windows или в виде приложения .xbap, потому что такие приложения сильно различаются по своей структуре. Приложения .xbap не могут использовать временные и диалоговые окна, поэтому обычно они организуются в виде навигационных приложений. Хотя приложе- ния .xbap изначально обладают ограниченными возможностями по сравнению с приложениями Windows, навигационная структура может подсказать интерес- ные идеи, и ограничения обернутся новыми возможностями. В последнем примере этой главы я решил переделать программу PlayJeuDe- Tacquin из главы 7 в формат XAML Browser Application. Новый проект называ- ется JeuDeTacquin и начинается с файла определения приложения. JeuDeTacquinApp.xaml <! _- ================================================== JeuDeTacquinApp.xaml (с) 2006 by Charles Petzold Application xml ns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Class="Petzold.JeuDeTacquin.JeuDeTacquinApp" StartupUri="JeuDeTacquin.xaml"> </Application> Файл JeuDeTacquin.xaml содержит корневой элемент Page, как и большинство приложений ХВАР, и описывает макет страницы. Я создал на странице заголо- вок, объект UniformGrid с именем unigrid, а также две кнопки Scramble и Next Larger. JeuDeTacquin.xaml <! - - =============================================== JeuDeTacquin.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Class="Petzold.JeuDeTacquin.JeuDeTacquin" WindowTitle="Jeu de Tacquin" Background="LightGray" Focusable="True" KeepAlive="True"> <Grid HorizontalAlignment="Center" Vertica1 Alignment="Center" Background="LightGray"> <Gri d.RowDefi ni ti ons> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Gri d.RowDefi ni ti ons>
622 Глава 22. Окна, страницы и перемещение <TextBlock Grid.Row="0" FontFamily="Garamond" FontSize="96" FontStyle="Italic" Margin="12"> Jeu de Tacquin </TextBlock> <Border Grid.Row=’T” BorderBrush="Black" BorderThickness = "1" HorizontalAlignment="Center" VerticalAlignment="Center"> <UniformGrid Name="unigrid" /> </Border> <Button Grid.Row="2" HorizontalAlignment="Left" Margin="12" MinWidth="lin" Click="ScrambleOnClick"> Scramble </Button> <Button Grid.Row="2" HorizontalAlignment="Right" Margin="12" Mi nWidth="li n” Cli ck=”NextOnCli ck"> Next Larger » </Button> <TextBlock Grid.Row="3" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="12"> (c) 2006 by Charles Petzold </TextBlock> </Grid> </Page> Класс JeuDeTacquin дополняется следующим файлом программной логики. Два поля, определяющие количество столбцов и строк, перестали быть константами, а в классе появилось дополнительное поле с именем isLoaded. Конструктор уста- навливает обработчик события Loaded и вызывает InitializeComponent. Напомню, что событие Loaded инициируется при каждом отображении Раде в процессе навигации. Обработчик события WindowOnLoaded инициализирует UniformGrid объектами Tile и объектом Empty. (Проект включает ссылки на Tile.cs и Empty.cs из исходного проекта PlayJeuDeTacquin). Обраттие внимание: свойству Title объекта Раде за- дается текст «Jeu de Tacquin» с указанием количества строк и столбцов в круглых скобках. Этот текст включается в журнальные списки кнопок Back и Forward. Обработчик события PageOnLoaded также задает поле isLoaded равным true, что- бы инициализация не проводилась заново для того же объекта Раде. JeuDeTacquin.xaml.cs //---------------------------------------------- // JeuDeTacquin.cs (с) 2006 by Charles Petzold //---------------------------------------------- using Petzold.PlayJeuDeTacquin; using System; using System.Windows; using System.Windows.Controls; using System.Wi ndows.Controls.Primitives;
Окна, страницы и перемещение 623 using System.Windows.Input: using System.Windows.Media; using System.Windows.Threading; namespace Petzold.JeuDeTacquin public partial class JeuDeTacquin : Page { public int NumberRows = 4; public int NumberCols = 4; bool isLoaded = false; int xEmpty, yEmpty, iCounter; Key[] keys = { Key.Left, Key.Right, Key.Up, Key.Down }; Random rand; UIEIement elEmptySpare = new EmptyO; public JeuDeTacquin() { Loaded += PageOnLoaded; InitializeComponent(); } void PageOnLoaded(object sender, RoutedEventArgs args) { if (! isLoaded) { Title = String.Format("Jeu de Tacquin ({0}\x00D7{l})", NumberCols, NumberRows); uni grid.Rows = NumberRows: uni grid.Columns = NumberCols; // Создание объектов Tile для заполнения всех ячеек, // кроме одной for (int i = 0; i < NumberRows * NumberCols - 1; i++) { Tile tile = new TileO; tile.Text = (i + 1).ToString(); tile.MouseLeftButtonDown += TileOnMouseLeftButtonDown; unigrid.Chi 1dren.Add(tile); } // Создание объекта Empty для заполнения последней ячейки unigrid.ChiIdren.Add(new Empty()); xEmpty = NumberCols - 1; yEmpty = NumberRows - 1; isLoaded = true; } Focus(); } void TileOnMouseLeftButtonDown(object sender, MouseButtonEventArgs args) { FocusO;
624 Глава 22. Окна, страницы и перемещение Tile tile = sender as Tile; int iMove = unigrid.Children.IndexOf(tile); int xMove = iMove % NumberCols; int yMove = iMove / NumberCols; if (xMove == xEmpty) while (yMove != yEmpty) MoveTile(xMove. yEmpty + (yMove - yEmpty) / Math.Abs(yMove - yEmpty)); if (yMove == yEmpty) while (xMove != xEmpty) MoveTile(xEmpty + (xMove - xEmpty) / Math.Abs(xMove - xEmpty). yMove); args.Handled = true; } protected override void OnKeyDown(KeyEventArgs args) { base.OnKeyDown(args): switch (args.Key) { case Key.Right: MoveTile(xEmpty - 1, yEmpty); break; case Key.Left: MoveTile(xEmpty + 1. yEmpty); break; case Key.Down: MoveTile(xEmpty. yEmpty - 1); break; case Key.Up: MoveTile(xEmpty. yEmpty +1); break: default: return: } args.Handled = true: void ScrambleOnClick(object sender. RoutedEventArgs args) { rand = new Random(): iCounter = 16 * NumberCols * NumberRows: DispatcherTimer tmr = new DispatcherTimer(): tmr.Interval = TimeSpan.FromMilliseconds(10); tmr.Tick += TimerOnTick: tmr.StartO: } void TimerOnTick(object sender, EventArgs args) for (int i = 0: i < 5: i++) { MoveTile(xEmpty. yEmpty + rand.Next(3) - 1); MoveTile(xEmpty + rand.Next(3) - 1. yEmpty): } if (0 == iCounter--) (sender as DispatcherTimer).Stop(); } void MoveTilednt xTile, int yTile)
Окна, страницы и перемещение 625 { if ((xTIle == xEmpty && yTile == yEnipty) 11 xTile < 0 || xTile >= NumberCols || yTile < 0 || yTile >= NumberRows) return; int 1T1le = NumberCols * yTile + xTile; int iEmpty = NumberCols * yEmpty + xEmpty; UIEIement elTile = unigrid.Children[iTile]; UIEIement elEmpty = unigrid.Children[iEmpty]; uni gri d.Chi 1dren.RemoveAt(i Ti1e); uni grid. Children. InsertdTile, elEmptySpare); uni gri d.Chi 1dren.RemoveAt(i Empty): uni grid.Chi 1dren.Insert(iEmpty. el Tile); xEmpty = xTile: yEmpty = yTile; elEmptySpare = elEmpty; } void NextOnClick(object sender. RoutedEventArgs args) { if ('Navigationservice.CanGoForward) { JeuDeTacquin page = new JeuDeTacquin(); page.NumberRows = NumberRows + 1; page.NumberCols = NumberCols + 1; Navigationservice.Navi gate(page); } else Navigationservice.GoForward(); } } } Во время преобразования программы в формат XBAP мш1 пришла в голову интересная новая возможность — кнопка Next Larger. Она реализуется обработ- чиком события, находящемся в самом конце файла С#. При первом щелчке на кнопке Next Larger программа создает новый объект типа JeuDeTacquin и уве- личивает количество строк и столбцов на 1 по сравнению с текущим. Переход к новой странице осуществляется вызовом Navigationservice.Navigate. Тем не менее предыдущая партия сохраняется и к ней можно обратиться при помощи кнопки Back. Если вернуться к этой игре и снова щелкнуть на кнопке Next Larger, вместо создания повой игры 5x5 происходит переход к ранее созданной игре. Это по- зволяет одновременно поддерживать сразу несколько игр с разными размерами. Мне просто не пришло в голову, что подобную возможность можно реализо- вать в предыдущей версии программы. Если команда меню для задания коли- чества строк и столбцов выглядит очевидно, о возможности создания сразу нескольких игр этого не скажешь. Более того, ее реализация была бы весьма гро- моздкой. Однако благодаря навигационным возможностям WPF такая функция реализуется практически тривиально.
Привязка данных Привязкой данных (data binding) называется методика связывания элементов управления и интерфейсных элементов с данными. Если такое определение по- кажется слишком расплывчатым, это объясняется лишь широкой областью при- менения и универсальностью этой методики. Привязки данных различаются в широком спектре, от простого подключения элемента CheckBox к логической переменной до сложного соединения базы данных с панелью ввода данных. Элементы всегда объединяли в себе две функции: отображение данных и воз- можность их изменения. Тем не менее в современных интерфейсах прикладно- го программирования эти стандартные связи между элементами и данными обычно автоматизируются. В прошлом программисту пришлось бы писать код инициализации объекта CheckBox по логической переменной, а затем задавать значение последней по объекту CheckBox, когда пользователь завершит работу с ним. В современных средах программирования разработчик определяет связь между элементом CheckBox и переменной, а механизм привязки автоматически решает обе задачи. Очень часто привязка данных может заменить обработчик события, что зна- чительно способствует упрощению кода — особенно при программировании на XAML. Привязка данных, определенная в XAML, может снять необходимость в определении обработчика событий в файле программной логики, а иногда и по- зволяет обойтись вообще без файла программной логики. В итоговом коде все происходит на уровне инициализации, а возможностей для ошибки остается го- раздо меньше. (Конечно, обработчики ошибок продолжают существовать, но они работают «за кулисами», и мы их получаем в уже отлаженном виде.) В привязке данных участвуют две стороны: источник и приемник. Как прави- ло, источником является некий набор данных, а приемником — элемент. На прак- тике различия между источником и приемником нередко стираются, а иногда даже происходит смена ролей, и приемник начинает поставлять данные источ- нику. Вероятно, простейший случай привязки встречается между двумя элементами. Предположим, мы хотим использовать элемент Label для просмотра свойства Value элемента ScrollBar. Конечно, можно установить обработчик события ValueChanged для элемента ScrollBar, но возможен и другой вариант с определением привязки данных, продемонстрированный в следующем автономном XAML-файле.
Привязка данных 627 BindLabelToScrollBar.xaml <! -- ====== BindLabelToScrollBar.xaml (с) 2006 by Charles Petzold «StackPanel xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <!-- Источник привязки --> «ScrollBar Name=”scroll” Orientation="Horizontal" Margin="24" Maximum="100" LargeChange="10" SmallChange="l" /> <!-- Приемник привязки --> «Label HorizontalAlignment="Center" Content="{Binding ElementName=scroll, Path=Value}" /> «/StackPanel> Привязка всегда назначается приемнику. В XAML-файле она задается для свойства Content элемента Label в следующем синтаксисе: Content="{Binding ElementName=scroll. Path=Value}" Binding, как и StaticResource с DynamicResource, является расширением разметки. Определение заключается в фигурные скобки. ElementName и Path — некоторые из свойств класса Binding, которые могут присутствовать в этом определении. В данном примере свойству ElementName задается значение scroll — имя, присво- енное ScrollBar в атрибуте Name. Свойство Path объекта Binding задано равным Value, что в данном контексте означает свойство Value объекта ScrollBar. Таким образом, свойство Content элемента Label привязывается к свойству Value элемен- та ScrollBar. При изменении состояния ScrollBar в элементе Label выводится его текущее значение. Сколько бы вы ни программировали на XAML, вам наверняка захочется по- местить кавычки в определение Binding. Элементы ElementName и Path так похожи на атрибуты XML, что ваши пальцы сами попытаются ввести что-нибудь вроде Content="{Binding ElementName="scrolT Path="Value"}" Но это абсолютно неверно! В фигурных скобках действуют совершенно иные правила. ElementName и Path не являются атрибутами XML. Здесь задействован всего один атрибут, и это Content. В фигурных скобках не только не должно быть фигурных скобок, но и конструкции ElementName и Path должны быть раз- делены запятыми. С другой стороны, если вы никак не избавитесь от привычки вводить кавыч- ки в определениях Binding, возможно, вы предпочтете альтернативный синтаксис элементов свойств. PropertyElementSyntax.xaml <! ======================================================== PropertyElementSyntax.xaml (с) 2006 by Charles Petzold
628 Глава 23. Привязка данных <StackPanel xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <!-- Источник привязки --> <ScrollBar Name="scroH" Orientation="Horizontal" Margin="24" Maximum="100" LargeChange="10" SmallChange="l" /> <!-- Приемник привязки --> <Label HorizontalAlignment="Center"> <Label.Content> <Binding ElementName="scroll" Path="Value" /> </Label.Content> </Label> </StackPanel> На этот раз элемент Binding располагается между начальным и конечным те- гами Label.Content, а свойства ElementName и Path интерпретируются как нор- мальные атрибуты XML. Возможно, вам захочется убрать вроде бы лишние теги Label.Content, но в данном случае они необходимы. Независимо от выбраного способа, элемент, в котором находится опреде- ление Binding, всегда является приемником привязки. Приемник должен быть производным от Dependencyobject. Свойство, для которого задается привязка, должно поддерживаться зависимым свойством. Следовательно, в нашем кон- кретном случае элемент Label должен содержать статическое открытое поле типа Dependencyproperty с именем Contentproperty. Требования к привязкам становятся еще более очевидными при изучении кода С#. Предположим, в программе созданы элементы ScrollBar и Label, сохра- ненные под именами scroll и 1Ы. Код выглядит так: Binding bind = new BindingO; bi nd.Source = scroll; bind.Path = new PropertyPath(ScrolIBar.ValueProperty): Ibl.SetBinding(Label.Content, bind); Фрагмент не является точным аналогом определения Binding в XAML, по- тому что он задает свойству Source объекта Binding объект ScrollBar вместо того, чтобы задать свойству ElementName строку «scroll». Но обратите внимание на вызов метода SetBinding для приемника привязки. Этот метод определяется клас- сом FrameworkElement, а его первый аргумент относится к типу DependencyPro- perty, и эти два факта означают, что приемником привязки может быть не лю- бой объект. Привязка должна устанавливаться для свойств, сопровождаемых зависимы- ми свойствами, потому что элементы управления и интерфейсные элементы спроектированы так, чтобы они реагировали па изменения своих зависимых свойств. Хотя из кода это и не очевидно, к источнику привязки предъявляется гораздо меньше требований. Связываемое свойство источника не обязано быть зависимым свойством. В идеальном случае свойство должно ассоциироваться
Привязка данных 629 с событием, указывающим на изменение свойства, но некоторые привязки нор- мально работают даже без оповещающего события. Давайте немного поэкспериментируем с файлом BindLabelToScrollBar.xaml. Хотя термины «источник» и «приемник» вроде бы подразумевают, что изменения в исходном элементе (в данном случае ScrollBar) инициируют изменения в при- емном элементе (Label), это лишь один из четырех возможных режимов привязки. Режим задается при помощи свойства Mode и членов перечисления BindingMode. По умолчанию используется следующий вариант: Content="{Binding ElementName=scroll, Path=Value. Mode=OneWay}" Обратите внимание: задание свойства Mode отделяется от Path запятой, а зна- чение OneWay не заключается в кавычки. Если вы предпочитаете синтаксис эле- ментов свойств, включите его в атрибуты элемента Binding: <Label.Content> <Binding ElementName="scroll" Path=,,Value" Mode="OneWay" /> </Label.Content> Также можно выбрать режим двусторонней привязки TwoWay: Content="{Binding ElementName=scroll. Path=Value. Mode=TwoWay}" В этом случае привязка работает так же, как в режиме OneWay, но теоретиче- ски изменения свойства Content объекта Label должны отражаться в свойстве Value объекта ScrollBar. /К вот еще одна возможность: Content="{Binding ElementName=scrol1, Path=Value, Mode=OneTime}" Режим OneTime означает, что приемник инициализируется по источнику, но не отслеживает дальнейшие изменения источника. В данной программе в эле- менте Label выводится значение 0, потому что исходное значение свойства Value объекта ScrollBar именно таково, но в будущем при изменениях ScrollBar надпись уже не изменится. (Вы можете явно задать свойство Value равным 50 в элементе ScrollBar; в этом случае в элементе Label будет содержаться значение 50, потому что он инициализируется этим значением.) Последний вариант: Content="{Binding ElementName=scroll, Path=Value, Mode=OneWayToSource}” Это самый запутанный вариант, в котором источник должен обновляться по данным приемника. В данном примере приемник (Label) должен обновлять ис- точник (ScrollBar), но Label не содержит числовых данных для передачи ScrollBar. При перемещениях полосы прокрутки надпись будет оставаться пустой. На первый взгляд режим OneWayToSource нарушает основные законы логики, но он может оказаться весьма полезным при создании привязок между двумя свойствами, когда приемное свойство не сопровождается зависимым свойством, а исходное — сопровождается. В этом случае закрепите привязку за источником и задайте свойству Mode значение OneWayToSource. Рассмотрим пример XAML-файла, в котором элементы ScrollBar и Label меня- ются местами.
630 Глава 23. Привязка данных BindScrollBarToLabel.xaml <1__ ======================================================= В1ndScrol1BarToLabel.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <!-- Приемник привязки --> <ScrollBar Orientation=”Horizontal" Margin="24" Maximum="100" LargeChange="10" SmallChange="T Value="{Binding ElementName=lbl, Path=Content}" /> <!-- Источник привязки --> <Label Name="lbl" Content="50" Horizontal Al ignment="Center" /> </StackPanel> Теперь Label является источником, a ScrollBar — приемником. Как обычно, оп- ределение Binding связывает свойство Value объекта ScrollBar со свойством Content объекта Label: Vaiue="{Binding ElementName=lbl. Path=Content}" Я также задал элементу Label значение Content равным 50. При запуске про- граммы в Label выводится текст 50, а бегунок ScrollBar находится в центре. При перемещении ScrollBar содержимое Label обновляется. Очевидно, по умолчанию используется двусторонняя привязка. Если задать ее явно, все будет работать точно так же: Value="{Binding ElementName=lbl. Path=Content, Mode=TwoWay}" Однако при переходе на одностороннюю привязку начинаются проблемы: Vaiue="{Binding ElementName=lbl. Path=Content. Mode=OneWay}" Объект ScrollBar инициализируется средней позицией бегунка, потому что он является приемником привязки, а в источнике Label указано значение 50. Но дальнейшие изменения отсутствуют, потому что ScrollBar не получает новых чи- словых данных от Label. Следующая конструкция приводит к тому же результату: Value="{Binding ElementName=lbl. Path=Content. Mode=OneTime}" Но попробуйте-ка этот вариант: Value="{Binding ElementName=lbl, Path=Content, Mode=OneWayToSource}“ На этот раз приемник ScrollBar управляет источником Label. Элемент Label ини- циализируется 0, потому что таково исходное значение ScrollBar. В дальнейшем Label исправно сообщает об изменениях в состоянии ScrollBar. Значение Mode по умолчанию определяется свойством, для которого опре- деляется привязка. Эксперименты с BindScrollBarToLabel.xaml показывают, что у свойства Value объекта ScrollBar по умолчанию используется режим TwoWay. Теоретически зависимое свойство ValueProperty должно содержать объект Frame-
Привязка данных 631 vvorkPropertyMetadata с истинным свойством BindsTwoWayByDefault. Тем не менее программа ExploreDependencyProperties из главы 16 показывает, что метаданные свойства ValueProperty полосы прокрутки хранятся в объекте PropertyMetadata, а не в FrameworkPropertyMetadata. По этой причине не стоит пытаться угадать режимы привязки по умолчанию. Свойство Mode — один из важнейших компонентов привязки, поэтому лучше внимательно проанализировать каждую привязку, определить подходящий ре- жим и задать его явно. Привязка данных уже встречалась в некоторых программах С#, встречавших- ся ранее в книге. В таких программах часто использовалось свойство DataContext, определяемое классом FrameworkElement, — альтернативный способ обозначе- ния объекта-источника, задействованного в привязке. Пример задания DataContext в XAML. BindingWithDataContext.xaml <i - - ========================================================= BindingWithDataContext.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentati on" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <!-- Источник привязки --> <ScrollBar Name="scroH" Orientation="Horizontal" Margin="24" Maximum="100" LargeChange="10" SmallChange="l" /> <!-- Приемник привязки --> <Label Horizontal Alignment="Center" DataContext="{Binding ElementName=scrol1}" Content="{Binding Path=Value}" /> </StackPanel> Обратите внимание: определение Binding задается атрибутам DataContext и Con- tent, то есть фактически разбивается надвое. В первом определении Binding ука- зывается атрибут ElementName, а во втором — Path. Конечно, в данном примере использование DataContext не дает никаких пре- имуществ, но в некоторых ситуациях это свойство оказывается чрезвычайно ценным. Значение DataContext наследуется по дереву элементов, и если задать его для одного элемента, оно также будет распространяться на всех потомков этого элемента. Файл TwoBindings.xaml показывает, как это делается. Свойство DataContext за- дается один раз для элемента StackPanel. Свойства элементов Label и Button при- вязываются к элементу ScrollBar. У Label в привязке участвует свойство Content, а у Button — свойство FontSize. При перемещении бегунка ScrollBar текст внутри кнопки постепенно увели- чивается, как и сама кнопка.
632 Глава 23. Привязка данных TwoBindings.xaml <।_ _ ============================================== TwoBindings.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" DataContext="{Bi ndi ng ElementName=scrol 1}"> < !-- Источник привязки --> <ScrollBar Name="scrol1" Orientation="Horizontal" Margin="24" Minimum^'!" Maximum="100" LargeChange="10" SmallChange="l" /> < !-- Приемники привязки --> <Label HorizontalAlignment="Center" Content="{Binding Path=Value, ModeOneWay}" /> < Button HorizontalAlignment="Center" Margin="24" FontSize="{Binding Path=Value, Mode=OneWay}"> Bound Button </Button> </StackPanel> В следующем автономном файле привязка данных используется для вывода текущей ширины и высоты клиентской области программы. WhatSize.xaml <।_ _ =========================================== WhatSize.xaml (с) 2006 by Charles Petzold <Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Name="grid"> <StackPanel Orientation='Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock Text="{Binding ElementName=grid, Path=ActualWidth}" /> <TextBlock Text=" &#x00D7; " /> <TextBlock Text="{Binding ElementName=grid, Path=ActualHeight}" /> <TextBlock Text=" device independent units" /> </StackPanel> </Grid> Панель StackPanel ориентирована горизонтально и содержит четыре элемента TextBlock, образующих одну строку текста. Два элемента TextBlock определяют привязки к свойствам Actual Width и ActualHeight объекта Grid. Свойства доступны только для чтения, поэтому режим привязки может быть только односторонним. Мои первые попытки написания программы WhatSize.xaml оказались безус- пешными. Вместо определения нескольких элементов TextBlock я сначала хотел использовать один элемент TextBlock с несколькими дочерними объектами типа
Привязка данных 633 —---------------------------------------------------------- Run. У ДВУХ элементов Run свойства Text привязывались к свойствам ActualWidth и ActualHeight панели Grid. Разметка выглядит так: <!-- Такая разметка не работает! --> <TextBlock> <Run Text="{B1nding ElementName=grid, Path=ActualWidth}" /> <Run Text=" &#x00D7: " /> <Run Text="{B1nding ElementName=grid, Path=ActualHeight}" /> <Run Text=" device Independent units" /> </TextBlock> фрагмент XAML мне казался нормальным, но в нем возникало исключение с сообщением «Объект тина ‘System.Windows.Data.Binding’ не может быть пре- образован к типу ‘System.String’» — очевидно, речь шла о типе свойства Text. Загадка решалась очень просто: свойство Text, определяемое в TextBlock, сопро- вождалось зависимым свойством с именем TextProperty. Свойство Text, опреде- ляемое классом Run, не поддерживается зависимым свойством. Приемник при- вязки данных должен быть зависимым свойством. Этот факт становится еще более очевидным при определении привязки в С#: первый аргумент SetBinding должен относиться к типу Dependencyproperty. Источник привязки не обязан быть зависимым свойством. Во всех примерах с привязкой данных, рассматривавшихся до настоящего момента, и приемник, и источник поддерживались зависимыми свойствами, но позднее будут при- ведены примеры, в которых источник привязки является самым обычным свой- ством .NET. В общем случае односторонняя привязка OneWay подразумевает постоянную передачу информации от источника к приемнику. Чтобы односто- ронняя привязка была успешной, источник должен реализовать некий механизм оповещения приемника об изменениях источника. Говоря о «некоем механизме», имею ли я в виду событие? Конечно, это одна из возможностей, но не единственная. Одной из основных областей применения для зависимых свойств была привязка данных, и система зависимых свойств обладает встроенной поддержкой оповещений. Формально источник привязки не обязан быть зависимым свойством, по это может быть весьма полезно. В следующем примере класс называется SimpleElement и объявляется произ- водным непосредственно от FrameworkElement. SimpleElement.cs Ц---------------------------------------------- И SimpleElement.cs (с) 2006 by Charles Petzold И---------------------------------------------- using System: using System.Globalization; using System.Windows; using System.Windows.Media; namespace Petzold.CustomElementBIndlng
634 Глава 23. Привязка данных class Simp]eElement : FrameworkElement { // Определение Dependencyproperty public static Dependencyproperty NumberProperty; // Создание Dependencyproperty в статическом конструкторе static SimpleElementC) { NumberProperty = DependencyProperty.RegisterCNumber". typeof(double). typeof(SimpleElement). new FrameworkPropertyMetadata(0.0. FrameworkPropertyMetadataOptions.AffectsRender)); // Dependencyproperty оформляется в виде свойства CLR public double Number set { SetValue(NumberProperty, value); } get { return (double)GetValue(NumberProperty); } } // Жесткое кодирование размера для MeasureOverride protected override Size MeasureOverride(Size sizeAvailable) { return new Size(200, 50); } // Метод OnRender просто выводит значение свойства Number protected override void OnRender(DrawingContext de) { de.Drawlext( new FormattedText(Number,ToString(). Cultureinfo.CurrentCulture. FlowDi recti on.LeftToRi ght. new TypefaceCTimes New Roman"), 12, SystemColors.Wi ndowTextBrush), new Point(0, 0)): } } Класс определяет свойство Number типа double, поддерживаемое свойством Dependencyproperty с именем NumberProperty. FrameworkPropertyMetadata всего лишь показывает, что значение по умолчанию равно 0, а при изменении свойства визуальное представление становится недействительным, что должно привести к вызову OnRender. Метод MeasureOverride использует очень простой подход к оп- ределению размеров и запрашивает размер в 200x50 аппаратно-независимых еди- ниц. Метод OnRender просто выводит свойство Number.
Привязка данных 635 Должен признаться, я в весьма скептическом настроении приступал к напи- санию разметки для экспериментов с привязкой между SimpleElement и ScrollBar. файл определения приложения, как обычно, устроен тривиально. CiistomElementBindingApp.xaml < ! - - ========================================================== CustomElementBIndlngApp.xaml (c) 2006 by Charles Petzold <Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" StartupUri="CustomElementBi ndi ngWi ndow.xaml"> < /Appl 1 cation> Элемент Window создает два элемента ScrollBar и два элемента SimpleElement, а также определяет три привязки между ними. CustomElementBindingWindow.xaml < ! - - ============================================================= CustomElementBindingWindow.xaml (c) 2006 by Charles Petzold < Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:src="clr-namespace:Petzold.CustomElementBi ndi ng" Title="Custom Element Binding Demo"> <StackPanel> <ScrolIBar Orientation="Horizontal" Margin="24" Maximuin="100" LargeChange="10" SmallChange="l" Value="{Binding ElementName=simple, Path=Number, Mode=OneWayToSource}" /> <src:SimpleElement x:Name="simple" HorizontalAlignment="Center" /> <ScrollBar Name="scrol1" Orientation="Horizontal" Margin="24" Maximum="100" LargeChange="10" SmallChange="l" Value="{Binding ElementName=simple. Path=Number. Mode=TwoWay}" /> <src:SimpleElement Horizonta1 Alignment="Center" Number="{Binding ElementName=scroll. Path=Value. Mode=OneWay}" /> </StackPanel> </Wi ndow>
636 Глава 23. Привязка данных Обратите внимание на атрибут x:Name первого элемента SimpleElement. Атри- бут x:Name предназначен для элементов XAML, которые не являются производ- ными от FrameworkElement, а следовательно, не имеют свойства Name. При попыт- ке использования Name я получил сообщение об ошибке, в котором говорилось: «Поскольку ‘SimpleElement’ реализуется в той же сборке, вместо атрибута Name необходимо задать атрибут x:Name». Так я и сделал. Первый объект ScrollBar определяет привязку типа OneWayToSource со свойст- вом Number первого элемента SimpleElement. Если щелкнуть на элементе ScrollBar и переместить его бегунок, первый элемент SimpleElement изменяется. Впрочем, ничего удивительного: мы и так ожидаем, что первый элемент ScrollBar будет ус- пешно обновлять первый SimpleElement. Второй элемент ScrollBar определяет двустороннюю привязку к свойству Number первого элемента SimpleElement; вы увидите, что она тоже работает. Хотя Simple- Element не имеет явного механизма оповещений, изменения свойства Number в ре- зультате манипуляций с первым элементом ScrollBar обнаруживаются привязкой, и второй элемент ScrollBar отслеживает состояние первого элемента ScrollBar. Так как привязка является двусторонней (TwoWay), манипуляции со вторым элемен- том ScrollBar также приводят к изменению SimpleElement (хотя первый элемент ScrollBar не изменяется). Второй элемент SimpleElement определяет одностороннюю привязку данных ко второму элементу ScrollBar. При перемещении бегунка второго элемента ScrollBar (вследствие привязки или вручную) второй элемент SimpleElement обновляется новым значением. Мораль проста и очевидна: определите Dependencyproperty и получите опове- щения бесплатно. Привязка данных также зависит от двух флагов метаданных. Если установить флаг FrameworkPropertyMetadataOptions.NotDataBindable, другие элементы все равно смогут привязываться к зависимому свойству, но вы не сможете определить при- вязку для самого зависимого свойства (другими словами, зависимое свойство с этим флагом не может быть приемником привязки данных). Флаг Framework- PropertyMetadataOptions.BindsTwoWayByDefault влияет только на привязки, у кото- рых зависимое свойство является приемником. Как вы уже видели, свойству Path объекта Binding задается свойство объекта- источника. Тогда почему оно называется Path, а не Property? Дело в том, что зна- чение может представлять собой нечто большее, чем простое свойство. Это мо- жет быть серия свойств (возможно, с индексами), объединенных точками — по аналогии с кодом С#, но без проблем с сильной типизацией. Далее приводится пример автономной XAML-программы с двумя такими свойствами. LongBindingPath.xaml <।_ _ ================================================== LongBindingPath.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"
Привязка данных 637 xml ns:s="clr-namespace:System;assembly=mscor 11b" FontS1ze="12pt" Name="page"> <StackPanel> <TextBlock Hor1zontalA11gnment="Center"> First element In StackPanel </TextBlock> <L1stBox HorizontalAlignment^’Center" Marg1n="24"> <L1stBoxItem>F1rst ListBox Item</L1stBoxItem> <L1stBoxItem>Second ListBox Item</L1stBoxItem> <L1stBoxItem>Th1rd LIstBoxItem</L1stBoxItem> <L1stBoxItem>Fourth ListBox Item</L1stBoxItem> <L1stBoxItem>F1fth LIstBoxItem</L1stBoxItem> </L1stBox> <TextBlock Hor1zontalA11gnment="Center"> <Label Content="Number of characters In third ListBox Item = " /> <Label Content="{B1nd1ng ElementName=page, Path=Content,Ch11dren[l].Items[2].Content.Length}" /> <L1neBreak /> <Label Content^'Number of characters In selected Item = " /> <Label Content="{B1nd1ng ElementName=page, Path=Content.Chi 1dren[l].Seiectedltern.Content.Length}" /> </TextBlock> </StackPanel> </Page> Единственным элементом, обладающим атрибутом Name, является корневой элемент. Страница содержит объект StackPanel, который, в свою очередь, содер- жит два элемента TextBlock, окружающие элемент ListBox. Последний содержит пять элементов ListBoxItem. Во встроенных элементах Label последнего объекта TextBlock выводится длина текста третьей позиции ListBox, а также длина текста выделенной позиции. Определение Path для второго из этих элементов выглядит так: Path=Content.Chi 1dren Е1J•Seiectedltem.Content.Length На первый взгляд похоже на строку вложенных свойств в коде С#, но пом- ните, что это всего лишь строка, и она разбирается как строка. Если парсеру не удается разобрать строку, процесс разбора просто прерывается без каких-ли- бо возражений или исключений. Слева направо, Path состоит из свойства Content страницы (то есть StackPanel), второго элемента коллекции Children объекта Stack- Panel (то есть ListBox), свойства Selectedltem объекта ListBox (ListBoxItem), свойства Content объекта ListBoxItem (строка) и свойства Length этой строки. Что вы почувствовали, когда осознали, что свойство Value в действительно- сти является вещественным числом двойной точности с дробным значением и множеством значащих цифр? Дело даже не в эстетике. Вещественный вывод
638 Г лава 23. Привязка данных ScrollBar может создать проблемы при привязке ScrollBar к элементу, ожидающе- му целые числа. В процессе передачи от источника к приемнику привязки (а иногда и обрат- но) данные могут преобразовываться от одного типа к другому. Класс Binding со- держит свойство Converter, в котором задается класс с методами Convert и Convert- Back для выполнения преобразования. Класс, выполняющий преобразование, должен реализовать интерфейс IValue- Converter. Он выглядит примерно так: public class MyConverter: IValueConverter { public object Convert(object value, Type typeTarget, object param, Cultureinfo culture) } Public object ConvertBack(object value, Type typeTarget, object param. Cultureinfo culture) } } Параметр value определяет преобразуемый объект, a typeTarget — тип, к кото- рому осуществляется преобразование. Это тип объекта, возвращаемого методом. Если метод не может вернуть объект такого типа, он должен возвращать null. Третий параметр определяет объект, соответствующий свойству ConvertParameter объекта Binding, а последний — культурный контекст, используемый при преоб- разовании (довольно часто он игнорируется). При создании привязок в коде C# свойству Convert задается объект класса, реализующего интерфейс IValueConverter: Binding bind = new BindingO: bi nd.Convert = new MyConverter(); Также можно задать свойству ConvertParameter объекта Binding объект, переда- ваемый в параметре param методам Convert и ConvertBack для управления преоб- разованием. В XAML процесс включения класса MyConverter в определение Binding оказы- вается менее прямолинейным. Сначала создается объект типа MyConverter, а за- тем ссылка на него включается в разметку. В главе 21 уже было показано, как подобные задачи решаются с использованием ресурсов. Итак, в секции Resources XAML-файла указывается класс, реализующий интерфейс IValueConverter, кото- рому назначается ключ: <src:MyConverter x:Key="conv" /> Предполагается, что определение MyConverter находится в исходном коде С#, а объявление пространства имен XML связывает src с пространством имен проекта.
Привязка данных 639 В определении Binding для ссылки на объект MyConverter используется расши- рение разметки StaticResource: "{Binding ... Convert={StaticResource conv}, ... }" Параметр Converterparameter позволяет передать дополнительную информа- цию, влияющую на ход преобразования. (Пример см. ниже.) Впрочем, это не единственный способ передачи дополнительной информации классу преобразо- вания. Класс должен реализовать интерфейс IValueConverter, но никто не запре- щает ему определять собственные открытые свойства. Это можно сделать в коде XAML при включении класса в секцию Resources XAML-файла: <src:MyConverter Decimals="4" s:Key="conv" /> Следующий класс обеспечивает преобразование double в decimal. Параметр преобразования задает количество цифр в дробной части, до которого округля- ется результат. DoubleToDecimalConverter.cs и И DoubleToDecimalConverter.cs (с) 2006 by Charles Petzold U---------------------------------------------------------- using System: using System.Globalization: using System.Windows: using System.Windows.Data; namespace Petzold.DecimalScrol1 Bar { [ValueConversion(typeof(double). typeof(decimal))] public class DoubleToDecimalConverter : IValueConverter { public object Convert(object value. Type typeTarget. object param. Cultureinfo culture) { decimal num = new Decimal((double)value): if (param != null) num = DecimaI.Round(num. Int32.Parse(param as string)); return num: } public object ConvertBack(object value, Type typeTarget. object param, Cultureinfo culture) return Decimal.ToDouble((decimal)value): } } } В документации WPF рекомендуется включить атрибут Valueconversion перед определением класса, чтобы оповестить средства разработчика о роли данного
640 Глава 23. Привязка данных класса. Решайте сами, насколько защищенными должны быть функции преоб- разования. Если вы используете их в своем собственном коде, то тип аргу- ментов в большой степени находится под вашим контролем. Например, метод Convert предполагает, что аргумент param представляет собой строку, преобра- зуемую в целое число. Также предполагается, что возвращаемое значение отно- сится к типу Decimal. Класс DoubleToDecimalConverter является частью проекта DecimalScrollBar, так- же включающего определение Window. DecimalScrollBarWindow.xaml <I_ _ ========================================================= DecimalScrollBarWindow.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:src="clr-namespace:Petzold.DecimalScrol1 Bar" Title="Decimal ScrollBar"> <Window.Resources> <src:DoubleToDecimalConverter x:Key="conv" /> </Window.Resources> <StackPanel> <!-- Источник привязки --> <ScrollBar Name="scroll" Orientation="Horizontal" Margin="24" Maximum="100" LargeChange="10" SmallChange="l" /> <!-- Приемник привязки --> <Label HorizontalAlignment="Center" Content="{Binding ElementName=scrol1. Path=Value, Converter={StaticResource conv}. ConverterParameter=2}" /> </StackPanel> </Window> В секции Resources класс DoubleToDecimalConverter ассоциируется с ключом «conv». Определение Binding в элементе Label обращается к классу преобразова- ния как к StaticResource и задает значение «2» свойству Converterparameter. Карти- ну дополняет файл определения приложения. DecimalScrollBarApp.xaml <।_ _ ====================================================== DecimalScrollBarApp.xaml (с) 2006 by Charles Petzold Application xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentati on" StartupUri="DecimalScrol1BarWindow.xaml"> </Application> При перемещении бегунка полосы прокрутки в этой программе значение отображается с двумя цифрами в дробной части.
Привязка данных 641 Одним из типов привязки, для которого всегда необходим класс преобразо- вания, является мужественная привязка, при которой информация нескольких объектов из разных источников консолидируется в одном объекте-приемнике. Преобразователь должен реализовать интерфейс IMultiValueConverter. Классиче- ский пример множественной привязки — объединение красной, зеленой и синей цветовых составляющих в одном объекте Color. Проект ColorScroll похож на проект ScrollCustomColors из главы 6, однако большая часть функциональности в нем была перемещена в код XAML. Напом- ню, что программа ScrollCustomColors содержит три полосы прокрутки для трех цветовых составляющих. В проекте ColorScroll используется механизм привяз- ки. Начнем с файла определения приложения. ColorScrollApp.xaml <! -- ==================================================== ColorScrollAppDef.xaml (с) 2006 by Charles Petzold «Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" StartupUri="ColorScrol1 Page.xaml"> </Appl ication> Просто для разнообразия я оформил главный XAML-файл в виде Раде вме- сто Window. В секции Resources определяются два класса преобразования — пер- вый преобразует значения double от полос прокрутки в байтовые данные для Label, а второй конструирует объект SolidColorBrush по трем составляющим. Как и в проекте ScrollCustomColors, макет содержит две панели Grid. Первая состоит из трех столбцов. В левом столбце расположена другая панель Grid с полосами прокрутки и надписями, в правом — элемент Border для отображения получен- ного цвета, а между ними — элемент GridSplitter. ColorScrollPage.xaml cl __ =============s=============r======================== ColorScrollPage.xaml (c) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:src="clr-namespace:Petzold.Co 1orScrol1" WindowTitle="Color Scrol1"> <Page.Resources> <src:DoubleToByteConverter x:Key="convDoubleToByte" /> <src:RgbToColorConverter x:Key="convRgbToColor" /> </Page.Resources> <Grid> Cri d.ColumnDefi ni ti ons> ColumnDefinition Width="*" /> ColumnDefinition Width="Auto" />
642 Глава 23. Привязка данных ColumnDefinition Width="*" /> </Gri d.ColumnDefi ni ti ons> <!-- Grid с элементами ScrollBar и Label --> CridSplitter Grid.Row="0" Grid. Col umn^T' Width=,,6" <Grid.RowDefinitions> <RowDefinition Height=,,Auto" /> <RowDefinition Height=,,100*" /> <RowDefinition Height=,,Auto" /> </Grid.RowDefinitions> <Gri d.ColumnDefi ni ti ons> <ColumnDefinition Width="33*" /> ColumnDefinition Width="33*" /> ColumnDefinition Width="33*" /> </G r id.ColumnDefi ni ti ons> <!-- Красный --> Cabel Content=,,Red" Foreground=,,Red" Hori zonta1Ali gnment="Center" Grid.Row="0" Grid.Column="0" /> CcrollBar Name="scrRed" Background="Red" Value=,,128" Minimum^'O" Мах1тит="255" SmallChange=,,l" LargeChange="16" Focusable=,,True" Grid.Row="l" Grid.Column^'O" /> <Label Content="{Binding ElementName=scrRed, Path=Value, Mode=OneWay, Converter={StaticResource convDoubleToByte}}" Horizontal Al ignment=,,Center" Grid.Row="2" Grid.Column="0" /> <!-- Зеленый --> Cabel Content=,,Green" Foreground="Green" Hori zontalAlignment="Center" Grid.Row="0" Grid.Columnar /> CcrollBar Name=,,scrGreen" Background=,,Green" Value="128" Minimum^'O" Мах1тит="255" SmallChange=,,l" LargeChange="16" Focusable="True" Grid.Row=,,l" Grid.Column=,,l" /> Cabel Content="{Binding ElementName=scrGreen, Path=Value, Mode=OneWay, Converter={StaticResource convDoubleToByte}}" Hori zontalAli gnment="Cente r" Grid.Row="2" Grid. Columnar /> <!-- Синий --> Cabel Content=,,Blue" Foreground=,,Blue" HorizontalAlignment="Center" Grid.Row="0" Grid.Column="2" />
Привязка данных 643 <ScrollBar Name="scrBlue" Background="Blue" Value="128" M1n1mum="0" Max1mum="255" SmallChange='T' LargeChange="16" Focusable=,,True" Gr1d.Row="r Gr1d.Column="2" /> <Label Content="{B1nd1ng ElementName=scrBlue( Path=Value, Mode=OneWay, Converter={Stat1cResource convDoubleToByte}}" HorizontalAl1gnment="Center" Gr1d.Row="2" Gr1d.Column="2" /> </Gr1d> <Gr1 dSpl 1 tter Gr1d.Row="0" Grid.Column="l" HorizontalAl1gnment="Center" VerticalAl1gnment="Stretch" /> <Border Gr1d.Row="0" Grid.Column="2"> <Border.Background> <MultlBlnding Converter="{Stat1cResource convRgbToColor}"> <B1nd1ng ElementName="scrRed" Path="Value" Mode="OneWay "/> <B1nd1ng ElementName="scrGreen" Path="Value" Mode="OneWay "/> <B1nd1ng ElementName="scrBlue" Path="Value" Mode="OneWay "/> </MultlBlnding> </Border.Background> </Border> </Gr1d> </Page> В трех элементах Label под тремя полосами прокрутки отображается теку- щее состояние полос, и у каждого элемента имеется привязка данных. Вот как выглядит объект Binding для красной составляющей, задающий свойство Content объекта Label по свойству Value объекта ScrollBar (элемент scrRed): Content="{Binding ElementName=scrRed, Path=Value, Mode=OneWay, Converter={Stat1cResource convDoubleToByte}}" Конвертор представляет собой статический ресурс с ключом «convDoubleTo- Byte», ссылающийся на следующий класс. DoubleToByteCon verier.ха ml //------------------------------------------------------- И DoubleToByteConverter.cs (с) 2006 by Charles Petzold И-------------------------------------------------------- using System; using System.Globalization; using System.Windows; using System.Windows.Data: namespace Petzold.ColorScroll {
644 Глава 23. Привязка данных [ValueConversion(typeof(double). typeof(byte))] public class DoubleToByteConverter : IValueConverter { public object Convert(object value. Type typeTarget, object param. Cultureinfo culture) return (byte)(double)value: } public object ConvertBack(object value. Type typeTarget. object param. Cultureinfo culture) return (double)value; } } Так как речь идет об односторонней привязке от источника к приемнику, метод ConvertBack может просто возвращать null, по я все равно реализовал его. В мето- де Convert необходимо сначала преобразовать объект в double, а затем в byte, а при попытке прямого преобразования объекта в byte произойдет исключение време- ни выполнения. Множественная привязка выполняется в конце ColorScrollPage.xaml, при зада- нии свойства Background элемента Border: <Border.Background> <MultiBinding Converter="{StaticResource convRgbToColor}”> <Binding ElementName=,,scrRed" Path=”Value” Mode="OneWay ”/> <B1nding ElementName="scrGreen" Path="Value" Mode=”OneWay "/> <Binding ElementName="scrBlue" Path=’,Value" Mode="OneWay "/> </Mul11 Binding> </Border.Background> Элемент MultiBinding всегда содержит один пли несколько дочерних элементов Binding. Конвертор, определяемый в секпип Resources файла и идентифицируе- мый ключом «convRgbToColor», должен получить три значения от трех привя- зок и преобразовать их в объект, подходящий для свойства Background элемента Border. Следующий класс реализует интерфейс IMultiValueConverter. RgbToColorCon verter. ха ml //----------------------------------------------------- // RgbToColorConverter.cs (c) 2006 by Charles Petzold //----------------------------------------------------- using System: using System.Globalization; using System.Windows; using System.Windows.Data; using System.Windows.Media; namespace Petzold.ColorScroll
Привязка данных 645 public class RgbToColorConverter : IMultiValueConverter { public object Convert(object[] value, Type typeTarget, object param, Cultureinfo culture) { Color clr = Color.FromRgb((byte)(double)value[0], (byte)(double)value[l], (byte)(double)value[2]); If (typeTarget == typeof(Color)) return clr: If (typeTarget == typeof(Brush)) return new SolidColorBrush(clr); return null: } public object[] ConvertBack(object value. Type[] typeTarget, object param, Cultureinfo culture) Color clr; object[] primaries = new object[3]; if (value is Color) clr = (Color) value; else if (value is SolidColorBrush) clr = (value as SolidColorBrush).Color; else return null; primaries[O] = clr.R; primariesEl] = clr.G: primaries[2] = clr.B; return primaries; } } } В первом параметре метода Convert передается массив объектов. Каждый до- черний элемент Binding элемента MultiBinding представлен одним объектом, а их количество не обязано быть фиксированным числом. Этот конкретный конвертор знает, что массив содержит три объекта типа double, поэтому он преобразует зна- чения к типу byte и конструирует объект Color. Чтобы код стал чуть более универ- сальным, я проверяю, какой тип обозначает параметр typeTarget (Color или Brush). Привязка является односторонней, поэтому метод ConvertBack не вызывается и может просто возвращать null. И все же я не устоял перед искушением выде- лить три цветовые составляющие из аргумента Color или SolidColorBrush и вер- нуть их в виде массива. Во всех привязках, которые встречались иаАм до настоящего момента, прием- ники немедленно обновлялись в соответствии с источниками. Однако такой ре- жим желателен не всегда, поэтому в отдельных случаях он не является операци- ей по умолчанию.
646 Глава 23. Привязка данных Следующий автономный XAML-файл содержит три пары элементов TextBox У каждой пары свойства Text связываются двусторонней привязкой. Элементы TextBox расположены так, чтобы слева находился источник, а справа — приемник BindToTextBox.xaml <।_ _ ================================================ BindToTextBox.xaml (с) 2006 by Charles Petzold <Grid xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Gri d.ColumnDefi ni ti ons> ColumnDefinition Width="*" /> ColumnDefinition Width="*" /> </Gri d.ColumnDefi ni ti ons> <Gri d.RowDefi ni ti ons> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Gri d.RowDefi ni ti ons> Cabel Grid.Row="0" Grid.Column="0" Margin="24 24 24 0" Content^'Source TextBox Controls" /> Cabel Grid.Row="0" Grid.Columnar Margin="24 24 24 0" Content="Target TextBox Controls" /> <TextBox Grid.Row="l" Grid.Column="0" Name="txtboxr Margin="24" /> <TextBox Grid.Row="l" Grid.Columnar Margin="24" Text="{Binding ElementName=txtboxl, Path=Text, Mode=TwoWay}" /> <TextBox Grid.Row="2" Grid.Column="0" Name="txtbox2" Margin="24" /> <TextBox Grid.Row="2" Grid.Columnar Margin="24" Text="{Binding ElementName=txtbox2, Path=Text, Mode=TwoWay}" /> <TextBox Grid.Row="3" Grid.Column="0" Name="txtbox3" Margin="24" /> <TextBox Grid.Row="3" Grid.Columnar Margin="24" Text="{Binding ElementName=txtbox3, Path=Text, Mode=TwoWay}" /> </Grid> Передайте фокус левому верхнему полю TextBox, щелкнув на нем. Поле явля- ется источником двусторонней привязки. Вводимые в нем символы также ото- бражаются в приемнике привязки — правом поле TextBox. Нажмите клавишу Tab;
Привязка данных 647 теперь фокус передается приемному полю TextBox. Хотя вводимый в нем текст отображается в поле, источник привязки не обновляется новым текстом. Тем не менее привязка явно задана как двусторонняя. Снова нажмите клавишу Tab. При потере фокуса правым приемным полем TextBox все содержимое приемника перемещается в источник. Привязка дей- ствительно является двусторонней, но обновление источника происходит лишь при потере фокуса приемником. Поэкспериментируйте с программой, нажимая клавишу Tab и вводя текст в разные поля. По умолчанию, когда свойство Text объекта TextBox является приемным свой- ством двусторонней привязки, источник привязки не обновляется вплоть до по- тери приемником фокуса ввода. Чтобы лучше понять логику происходящего, представьте, что источником в программе BindToTextBox является не элемент TextBox, а база данных, с которой взаимодействует программа. Правое поле TextBox позволяет просматривать и редактировать записи базы данных. Любые измене- ния в базе данных должны отображаться немедленно, поэтохму свойство Text при- емного объекта TextBox обновляется с каждым изменением источника. Но должна ли база данных обновляться с каждым нажатием клавиши при редактировании поля, включая все опечатки и случайные переходы? Вероятно, пет. Источник должен обновляться только после того, как ввод текста будет завершен, но при- вязка может выявить завершение ввода только одним простым способом — при потере фокуса элементом TextBox. Описанное поведение можно изменить при помощи свойства UpdateSourceTrigger объекта Binding. Свойству задается одно из значений перечисления UpdateSource- Trigger — LostFocus (используется по умолчанию для свойства Text элемента Text- Box), PropertyChanged (подходит для большинства свойств) или Explicit (измене- ния отражаются в источнике лишь после специальных действий со стороны программы). Попробуйте заменить одну из привязок BindToTextBox.xaml следующим фраг- ментом: Text="{Binding ElementName=txtboxl, Path=Text, Mode=TwoWay. UpdateSourceT rigger=PropertyChanged}" Теперь источник изменяется с каждым нажатием клавиши в приемном эле- менте TextBox. С режимом UpdateSourceTrigger.Explicit придется немного потрудиться. Про- грамма, в котором используется этот режим, должна вызвать GetBindingExpression (метод, определяемый классом FrameworkElement) для элемента, участвующего в определении привязки. Аргументом является объект Dependencyproperty, задей- ствованный в привязке: BlndingExpresslon bindexp = txtboxSource.GetBindi ngExpresslon(TextBox.TextProperty): Когда потребуется обновить источник по приемнику (например, в результате нажатия кнопки Update), включите в программу вызов bindexp.UpdateSource();
648 Глава 23. Привязка данных Вызов не отменяет действующий режим привязки. Последний должен быть равен TwoWay или OneWayToSource, или вызов будет проигнорирован. Если мы хотим думать о привязке в контексте баз данных и внешних клас- сов/объектов, нам придется отказаться от использования свойства ElementName. ElementName хорошо подходит для связывания двух элементов, но мы должны выйти за рамки этого ограничения. Вместо того чтобы задавать источник привязки свойством ElementName, в боль- шинстве примеров в оставшейся части главы будет использоваться свойство Source. Свойство Source ссылается на объект, а свойство Path — па свойство (или цепоч- ку свойств) этого объекта. В частности, при задании Source может использоваться расширение разметки x:Static. Как говорилось в главе 21, x:Static позволяет XAML-файлам обращаться к статическим полям и свойствам классов. В некоторых случаях (например, для статических свойств класса Environment) для обращения к свойствам достаточно одной конструкции x:Static. Однако вполне возможно, что на самом деле вас ин- тересует свойство объекта, па который ссылается статическое' свойство. В подоб- ных ситуациях следует использовать привязку. Для примера возьмем свойство DayNames класса DateTimeFormatlnfo в простран- стве имен System.Globalization. DayNames возвращает массив из семи строк, пред- ставляющих названия семи дней педели. Для обращения к DayNames потребует- ся объект типа DateTimeFormatlnfo, а класс DateTimeFormatlnfo сам предоставляет два статических свойства для получения экземпляров класса. Статическое свой- ство DateTimeFormatlnfo.Invariantinfo возвращает экземпляр DateTimeFormatlnfo для «универсального» культурного контекста, a DateTimeFormatlnfo.Currentinfo возвра- щает экземпляр, подходящий для культурного контекста пользователя. Следующий фрагмент C# сохраняет массив строк с названиями дней педели па языке пользователя: stringf] strDayNames = DateTimeFormatlnfo.CurrentJnfo.DayNames: Для получения доступа к этой информации в XAML необходимо определить привязку. Свойство Source содержи! расширение разметки x:Static для статиче- ского свойства DateTimeFormatlnfo.Currentlnfo. Свойство Path разметки содержит строку «DayNames». Также XAML-файл должен содержать объявление про- странства имен XML, связывающее пространство System.Globalization с префик- сом (например, д). DaysOfWeek.xaml <।__ ============================================= DaysOfWeek.xaml (с) 2006 by Charles Petzold <StackPanel xmlns=”http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x=”http://schemas.microsoft.com/wi nfx/2006/xaml ” xml ns:g="clr-namespace:System.G1oba1ization:assembly=mscorlib"> <!-- Привязка свойства ItemsSource объекта ListBox к свойству DayNames объекта DateTimeFormatlnfo. -->
Привязка данных 649 <ListBox Name=”Istbox" Ног1zonta1 Aljgnment="Center" Margi №"24” ItemsSource="{Binding Source={x:Static g:DateTimeFormatInfo.Current Info}. Path=DayNanies. Mode=OneTime}" !> <!-- Привязка свойства Text объекта TextBlock к свойству Selectedltem объекта ListBox --> <TextBlock HorizontalAl ignment="Center” Text="{Bi ndi ng ElementName=lstbox, Path=SelectedlLem. Mode=OneWay}" /> </StackPanel> Фактически файл содержит две привязки. Первая получает массив DayNames и задает его свойству ItemsSource объекта ListBox; в результате список заполняется названиями дней недели. Вторая привязка выводит текущую выделенную стро- ку ListBox в объекте TextBlock. У первой привязки свойству Mode задается режим OneTime. Никакое другое значение не подойдет, потому что класс DateTimeFormatlnfo не может выдать опо- вещение об изменениях в названиях дней педели. Файл использует синтаксис Binding для обращения к конкретному свойству, но на самом деле ему нужно по- лучить данные только один раз. Привязки обычно подразумевают постоянную передачу информации от ис- точника привязки к приемнику; для этого должен существовать механизм опо- вещения приемника об изменения свойства источника. Если источник является зависимым свойством, вы получаете этот механизм «бесплатно». Ранее в этой главе приводился класс SimpleElement, производный от Frame- workElement, в котором определялось зависимое свойство с именем Number. Этот класс участвовал в привязке данных без каких-либо дополнительных усилий с нашей стороны. Наследование от FrameworkElement не является обязательным для определения зависимых свойств. Если источник данных не является визу- альным объектом, вместо этого можно наследовать от Dependencyobject — класса, определяющего методы SetValue и GetValue, необходимые для реализации зависи- мых свойств. Предположим, мы хотим написать небольшую программу с цифровыми часа- ми. Разумеется, визуальное оформление будет реализовано в XAML, но для ча- сов также потребуется источник текущей даты/врсмени и механизм оповещения об изменениях времени (например, ежесекундного — для простоты). Следующий класс, производный от DependencyObject, определяет свойство DateTime, поддер- живаемое зависимым свойством DateTimeProperty. ClockTickerl.cs //--------------------------------------------- // ClockTickerl.cs (с) 2006 by Charles Petzold
650 Глава 23. Привязка данных using System: using System.Windows: using System.Windows.Threading; namespace Petzold.DlgltalClock { public class ClockTickerl : Dependencyobject { // Определение Dependencyproperty public static Dependencyproperty DateTImeProperty = DependencyProperty.Register("DateT1me", typeof(DateT1 me), typeof(ClockTIckerl)): // Доступ к Dependencyproperty как к свойству CLR public DateTime DateTime { set { SetValue(DateT1meProperty. value); } get { return (DateTime) GetValue(DateTimeProperty): } } // Конструктор запускает таймер public ClockTIckerK) { DispatcherTimer timer = new D1spatcherT1mer(); timer.Tick += TimerOnTick: timer.Interval = TlmeSpan.FromSeconds(l); timer.StartO: } // Обработчик события таймера задает свойство DateTime void T1merOnT1ck(object sender, EventArgs args) { DateTime = DateTime.Now; } } } Для меня самой интересной частью класса ClockTickerl является обработчик события объекта DispatcherTimer. Он просто задает свое свойство DateTime равным DateTime.Now. Чтобы понять, какое действие окажет вызов SetValue (инициируе- мый заданием свойства DateTime), необходимо рассмотреть код XAML проекта DigitalClock. Начнем с файла определения приложения. DigitalClockApp.xaml <।_ _ ================================================== DigitalClockApp.xaml (с) 2006 by Charles Petzold Application xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" StartupUr1="D1g1talClockW1ndow.xaml" />
Привязка данных 651 Для определения привязки данных файл Window должен сослаться на класс ClockTickerl. Класс ClockTickerl не является производным от FrameworkElement, по- этому элемент Binding не может использовать атрибут ElementName — вместо это- го необходимо использовать Source. В данной ситуации атрибут Source должен ссылаться на объект типа ClockTickerl, а мы уже знаем, как сослаться на объект в разметке: следует определить его в секции Resources и обратиться к нему через расширение StaticResource. pjgitalClockWindow.xaml <! - - ===================================================== Digital ClockWindow.xaml (c) 2006 by Charles Petzold <Window xml ns=,,http://schemas, mi crosoft. com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/wi nfx/2006/xaml" xml ns:src="clr-namespace:Petzold.Di gi tai Clock" Title="Digital Clock" SizeToContent="WidthAndHeight" Resi zeMode="CanMi ni mi ze" FontFamily="Bookman Old Style" FontSize="36pt"> <Window.Resources> <src:ClockTickerl x:Key="clock" /> </Window.Resources> <Window.Content> <B1nding Source="{StaticResource clock}" Path="DateTime" /> </Window.Content> </Window> Секция Resources содержит определение ClockTickerl, ассоциированное с клю- чом «clock». Свойству Content задается конструкция Binding, у которой свойство Source соответствует статическому ресурсу с именем «clock», a Path ссылается на свойство DateTime объекта ClockTickerl. Программа выводит дату и время в формате по умолчанию, зависящем от ре- гиональных настроек. Выводится только короткая версия даты/времени, без се- кунд. Чтобы убедиться в том, что привязка данных действительно работает, вам придется минуту подождать. Вероятно, определение зависимого свойства является наиболее эффективным способом, обеспечивающим использование класса в качестве источника привяз- ки данных, но это не единственный вариант. Более традиционный подход осно- ван на определении события. При этом событие должно быть определено так, чтобы оно было успешно обнаружено логикой привязки в WPF. Если свойство Whatever должно использоваться в качестве источника привязки данных, то та- кое событие должно называться Wftflteae/Changed. В классе ClockTicker2 для такого события следует сначала исключить зависимое свойство DateTimeProperty, а затем определить открытое событие DateTimeChanged: public event EventHandler DateTimeChanged;
652 Глава 23. Привязка данных Замените определение свойства DateTime следующим: public DateTime DateTime { get { return DateTime.Now; } Конструктор сохраняет прежний вид, но обработчик события TimerOnTick дол- жен инициировать событие DateTimeChanged: void TimerOnTick(object sender, EventArgs args) { if (DateTimeChanged != null) DateT1meChanged(th1s, new EventArgsO); } На этом все изменения заканчиваются. Логика привязки данных WPF также успешно обнаружит событие, определяе- мое в классе, реализующем интерфейс INotifyPropertyChanged. Интерфейс требует, чтобы класс определял событие PropertyChanged иа базе делегата PropertyChanged- EventHandler: public event PropertyChangedEventHandler PropertyChanged; При инициировании события PropertyChanged в первом аргументе передается this (как обычно), а во втором — объект тина PropertyChangedEventArgs, кото- рый является производным от EventArgs и определяет дополнительное свойство PropertyName типа string. Свойство PropertyName идентифицирует изменяемое со- бытие. В PropertyChangedEventArgs также определяется конструктор со строковым аргументом для задания PropertyName. Итак, если класс определяет свойство DateTime, он может инициировать со- бытие PropertyChanged, сообщающее об изменении DateTime, следующим образом: PropertyChanged(this, new PropertyChangedEventArgs("DateTime")); Такой способ хорошо подходит для классов с большим количеством изменяе- мых свойств, потому что все они могут обслуживаться одним событием Property- Changed. Впрочем, для одного изменяемого свойства он тоже подойдет. Класс ClockTicker2 показывает, как использовать событие PropertyChanged для передачи информации об изменении свойства DateTime. ClockTicker2.cs //--------------------------------------------- // ClockTicker2.cs (с) 2006 by Charles Petzold //--------------------------------------------- using System; using System.ComponentModel; using System.Windows; using System.Windows.Thread!ng;
Привязка данных 653 namespace Petzold.FormattedDi gi tai Clock public class ClockT1cker2 : INotifyPropertyChanged { • // Событие, необходимое для интерфейса INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged: // Открытое свойство public DateTime DateTime { get { return DateTime.Now: } // Установка таймера в конструкторе public ClockTIcker2() { DIspatcherTimer timer = new D1spatcherT1mer(): timer.Tick »= TimerOnTick: timer.Interval = TlmeSpan.FromSeconds(l); timer.Start(): } // Обработчик собьпия таймера инициирует событие PropertyChanged void TimerOnTick(object sender. EventArgs args) ir (PropertyChanged != null) PropertyChanged(this. new PropertyChangedEventArgs("DateT1me")): } } } Класс ClockTicker2 является частью нового проекта FormattedDigitalClock, ко- торый не только использует альтернативный способ оповещения об изменениях свойства DateTime, ио и предоставляет автору кода XAML большую гибкость при форматировании объекта DateTime. Формат даты и времени в программе DigitalClock определялся вызовом То- String для объекта DateTime. Однако в классе DateTime определяется перегружен- ная версия метода ToString, которая получает дополнительный аргумент с фор- матной строкой и может выводить дату и время в разных форматах. Например, в формате «Т» выводит время с секундами, а в формате «Ь> - время без секунд. Конечно, было бы удобно использовать форматные строки прямо в XAML-фай- ле. Именно для решения этой задачи был создан класс FormattedTextConverter. Он реализует интерфейс IValueConverter, поэтому он может использоваться для задания свойства Converter в привязках данных. Свойство ConverterPa га meter при- вязки задает форматную строку тем же способом, как при использовании в String. Format, потому что его содержимое используется методом Convert для преобразо- вания объекта в строку.
654 Глава 23. Привязка данных FormattedTextConverter.cs // // FormattedTextConverter.cs (с) 2006 by Charles Petzold // using System; using System.Globalization: using System.Windows: using System.Windows.Data: namespace Petzold.FormattedDigitai Clock { public class FormattedTextConverter : IValueConverter { public object Convert(object value, Type typeTarget, object param, Cultureinfo culture) { if (param is string) return String.Format(param as string, value): return value.ToStringO: } public object ConvertBackCobject value, Type typeTarget, object param, Cultureinfo culture) { return null: } } } Класс FormattedTextConverter может использоваться с объектами любых типов. Логичнее всего использовать его для объектов с несколькими вариантами фор- матирования (например, целыми или вещественными числами), но он может использоваться и для включения другого текста в объект. В файле FormattedDigitalClockWindow.xaml свойству Converterparameter задается строка «... {0:Т} ...», чтобы время с секундами выводилось между многоточиями. FormattedDigitalClockWindow.xaml <।_ _ ============================================================== FormattedDigitalClockWindow.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:src="clr- namespace:Petzold.FormattedDigi ta 1 Cl ock" Title="Formatted Digital Clock" Si zeToContent="Wi dthAndHei ght" Resi zeMode="CanMi ni mi ze" FontFamily="Bookman Old Style" FontSize="36pt"> <Window.Resources> <src:ClockTicker2 x:Key="clock" />
Привязка данных 655 <src:FormattedTextConverter x:Key="conv" /> </Window.Resources> <Window.Content> <Binding Source="{StaticResource clock}" Path="DateTime" Converter="{Stat1cResource conv}" ConverterParameter="... {0:T} ..." /> </Window.Content> </Window> Обратите внимание: в форматной спецификации используются те же фигурные скобки, которые используются для обозначения расширений разметки XAML! Свойство Converterparameter не может быть задано в следующем виде (без точек и пробелов, в отличие от предыдущего примера): ConverterParameter="{0:T}" В таких случаях необходимо использовать префикс из пары фигурных скобок: ConverterParameter="{}{0:Т}" Остается рассмотреть файл определения приложения для проекта Formatted- DigitalClock. FormattedDigitalClockApp.xaml <! - - =========================================================== FormattedDigitalClockApp.xaml (с) 2006 by Charles Petzold <Appl ication xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" StartupUri="FormattedDigi tai ClockWi ndow.xaml" /> Конечно, если вы действительно серьезно относитесь к использованию фор- матных строк .NET в XAML, последовательных преобразований отдельных объ- ектов вам будет недостаточно, потому что у функций Console.Write и String.Format таких ограничений не существует. Было бы удобно иметь возможность форма- тирования нескольких объектов в одну строку. Для решения подобных задач применяются множественные привязки (MultiBinding). Как было показано в про- грамме ColorScroll, несколько источников можно привязать к одному приемни- ку при помощи класса-конвертора, реализующего интерфейс IMultiValueConverter. В следующем классе используется параметр param с форматной строкой и мас- сив объектов value, содержащий форматируемые объекты. Оба параметра просто передаются при вызове String.Format. FormattedMultiTextConverter.es И-------------------------------------------------------------- И FormattedMultiTextconverter.cs (с) 2006 by Charles Petzold Ц-------------------------------------------------------------- using System: using System.Globalization: using System.Windows: using System.Windows.Data:
656 Глава 23. Привязка данных namespace Petzold.Env1ronmentInfo2 public class FormattedMultiTextConverter : IMultIValueConverter { public object Convert(object[] value, Type typeTarget, object param, Cultureinfo culture) { return String.Format((string) param, value); } public objects ConvertBackCobject value, Type[] typeTarget, object param, Cultureinfo culture) return null; } } Чтобы продемонстрировать работу этого класса, вернемся к задаче из преды- дущей главы: форматированию статических свойств класса Environment. В пре- дыдущем решении для каждого фрагмента текста использовался отдельный объект TextBlock. В программе Environmentlnfo2 используется единый объект TextBlock, свойство Text которого связано с объектом MultiBinding. Разумеется, свой- ство Converter ссылается на класс FormattedMultiTextConverter. EnvironmentlnfoZWindow.xaml <।_ _ ========================================================= EnvironmentInfo2W1ndow.xaml (c) 2006 by Charles Petzold <W1ndow xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml" xml ns:s="clr-namespace;System;assembly=mscorl1b" xml ns:src="clr-namespace:Petzold.EnvlronmentInfo2" T1tle="Environment Info"> <W1ndow.Resources> <src:FormattedMultiTextConverter x:Key="conv" /> </W1ndow.Resources> <TextBlock> <TextBlock.Text> <MultiBlnding Converter^"(StaticResource conv}" ConverterParameter= "Operating System Version: {0} &#x000A;.NET Version: (1} &#x000A:Machine Name: {2} &#x000A:User Name: {3} &#x000A;User Domain Name: {4} &#x000A;System Directory: {5} &#x000A;Current Directory: {6} &#x000A:Command Line: (7}" > <B1nd1ng Source="{x:Stat1c s:Env1ronment.0SVers1on}" />
Привязка данных 657 <Binding Source="{x:Static s:Environment.Version}" /> <Binding Source="{x:Static s:Environment.MachineName}" /> <Binding Source="{x:Static s:Environment.UserName}" /> <Binding Source="{x:Static s:Envi ronment.UserDomainName}" /> <B1nding Source="{x:Static s:Envi ronment.SystemDi rectory}" /> <Binding Source="{x:Static s:Envi ronment.CurrentDi rectory}" Л <Binding Source="{x:Static s:Environment.CommandLine}" /> </Mult1 Binding> </TextBlock.Text> </TextBlock> </Window> Значение Converterparameter представляет собой длинную последовательность символов, разбитую на несколько строк. Обратите внимание: переводы строк за- даются в стиле XML («&#х000А;»), а не в стиле C# и выравниваются по левому краю, чтобы избежать появления в строке нежелательных пробелов. Также обратите внимание на то, что элементы Binding не требуют атрибутов Path, потому что нас интересует само значение Source, а не свойства Source. Так- же можно задать свойству Path пустую строку: Path="" Далее приводится файл определения приложения для проекта. EnvironmentlnfoZApp.xaml <i __ ====================================================== EnvironmentInfo2App.xaml (c) 2006 by Charles Petzold application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" StartupUri="EnvironmentInfo2Window.xaml" /> Итак, мы рассмотрели вариант разметки Binding с использованием ElementName и Source. Существует только одно альтернативное решение: свойство Relative- Source, содержащее ссылку на один из предков в дереве элементов или на сам элемент. В файле RelativeSourceDemo.xaml продемонстрированы три привязки дан- ных на базе RelativeSource. RelativeSourceDemo.xaml <i __ ===================================================== RelativeSourceDemo.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi n fx/2006/xaml" TextBlock.FontSize="12" > <StackPanel Orientation="Horizontal" HorizontalAl ignment=,,Center"> <TextBlock Text="This TextBlock has a FontFamily of " /> <TextBlock Text="{Binding RelativeSource={RelativeSource self},
658 Глава 23. Привязка данных Path=FontFami1у}" /> <TextBlock Text=" and a FontSize of " /> <TextBlock Text="{Binding RelativeSource={RelativeSource self}, Path=FontSize}" /> </StackPanel> <StackPanel Orientation="Horizontal" HorizontalA11gnment="Center"> <TextBlock Text="Th1s TextBlock is inside a StackPanel with " /> <TextBlock Text= "{Binding RelativeSource={RelativeSource AncestorType={x:Type StackPanel}}, Path=Orientation}" /> <TextBlock Text=" orientation" /> </StackPanel> <StackPanel Orientation="Horizontal" Horizontal Alignment="Center"> <TextBlock Text="The parent StackPanel has " /> <TextBlock Text= "{Binding Relati veSource={RelativeSource AncestorType={x:Type StackPanel}, AncestorLevel=2}, Path=Orientation}" /> <TextBlock Text=" orientation" /> </StackPanel> </StackPanel> Ни в одном элементе этого файла не используется атрибут Name. В первом случае используется расширение разметки Relativesource, которое позволяет при- вязке ссылаться на тот элемент, в котором находится привязка: RelativeSource={RelativeSource seif} В данном случае элемент TextBlock отображает информацию о собственном шрифте. Далее в другом элементе TextBlock отображается свойство Orientation па- нели StackPanel, с использованием расширения разметки RelativeSource вида Relati veSource={Relati veSource AncestorType={x:Type StackPanel}} Однако эта панель StackPanel вложена в другую. Чтобы добраться до дру- гой панели StackPanel, расположенной выше в цепочке, нам потребуется свойст- во AncestorLevel, использованное в третьей привязке: RelativeSource={RelativeSource AncestorType={x:Type StackPanel}, AncestorLevel=2} В этой главе были рассмотрены различные формы и способы использования элемента Binding. Привязка данных играет важную роль в программах, которые выводят информацию из баз данных, а также предоставляют возможность редак- тирования существующих и ввода новых данных. Некоторые специфические прие- мы, используемые при программировании таких приложений, описаны в главе 26.
24 Стили Секции Resources в XAML-файлах удобны для определения различных объек- тов, ссылки на которые используются в разметке, многие ресурсные секции ис- пользуются исключительно для определения стилевых объектов Style. В сущно- сти, стиль представляет собой совокупность значений свойств, применяемых к элементам. Стили отчасти компенсируют невозможность использования в XAML цик- лов для создания нескольких элементов с идентичными свойствами. Предполо- жим, страница содержит несколько кнопок, которые должны обладать рядом об- щих свойств — например, все кнопки должны обладать одинаковым свойством Margin, одинаковым шрифтом и т. д. В этом отношении стили по своему назна- чению и функциональности напоминают таблицы стилей Microsoft Word или таблицы CSS в HTML. Но реализация стилей в WPF обладает более широкими возможностями, потому что вы также можете задать изменения свойств, ини- циируемые по изменению других свойств или возникновению событий. Класс Style определяется в пространстве имен System.Windows. Он является производным от Object и не имеет потомков. Самое важное свойство Style называ- ется Setters. Оно относится к типу SetterBaseCollection — коллекции объектов Setter- Base. Абстрактный класс SetterBase является базовым для Setter и Eventsetter. Элементы Setter и Eventsetter являются дочерними по отношению к элемен- ту Style: <Stylе .. .> <Setter ... /> <EventSetter ... /> <Setter ... /> </Sty1e> В определениях Style объекты Setter обычно встречаются гораздо чаще объек- тов EventSetter. В сущности, Setter связывает свойство с конкретным значением. Важнейшими свойствами класса Setter являются свойства Property (тип Dependency- Property) и Value (тип object). В коде XAML объект Setter выглядит так: <Setter Property="Control.FontSize" Value="24" /> Хотя атрибут Property всегда ссылается на зависимое свойство, обратите внимание на то, что при задании свойства использовано имя FontSize вместо
660 Глава 24. Стили FontSizeProperty. Имени свойства обычно (хотя и не всегда) должно предшество- вать имя класса, в котором это свойство определяется пли наследуется. В про- граммном коде должно использоваться фактическое имя зависимого свойства которому всегда предшествует имя класса. Чтобы задать значение null для атрибута Value, воспользуйтесь расширением разметки x:Null: Vaiue="{x:Nul1}" Классы FrameworkElement и FrameworkContentElement определяют свойство Style типа Style, поэтому теоретически возможно (хотя и не особенно полезно) опре- делить элемент Style, локальный для элемента, к которому применяется стиль. Например, следующий автономный XAML-файл определяет элемент Style, ло- кальный для элемента Button. Button WithLocalStylexaml ButtonWithLocalStyle.xaml (c) 2006 by Charles Petzold ^Button xmlns^"http://schemas.microsoft.com/winfx/2006/xaml/presentation" HorizontaIA1 ignment=,,Center" VeriicalAlignment="Center" Foregrourid="Red"> <BuLton.SLyle> <Style> <Setter Property="Button.FontSize" Value="18pt" /> <Setter Property="Control.Foreground" Value="Blue" /> </Style> </Button.Style> Button with Local Style </Button> Элемент Style включает два элемента Setter для определения свойств FontSize и Foreground кнопки. Обратите внимание: имя первого задастся в форме Button. FontSize, а второго — в форме Control.Foreground. В действительности имя класса не столь существенно, потому что Button наследует свойство Foreground от Control. Даже если заменить имена на TextBlock.FontSize и TextBlock.Foreground, программа будет работать точно так же. Несмотря на отсутствие наследования между Text- Block и Button, в классах TextBlock и Control определяются два свойства, основан- ных на исходных определениях из TextElement. Также обратите внимание на то, что в соответствии с определением Style свой- ство Foreground должно быть равно Blue, но свойству Foreground в начале файла явно задается значение Red. Какой цвет будет использоваться? Цветом текста кнопки будет красный, потому что локальные настройки (этим термином обо- значаются свойства, заданные непосредственно в элементе) замещают настрой- ки стиля. С другой стороны, стили замещают свойства, унаследованные по ви- зуальному дереву. На практике стили обычно определяются в ресурсных секциях, чтобы они мог- ли совместно использоваться разными элементами. Стили, как и другие ресурсы,
Стили____________________________________________________________661 —.--------------------------- идентифицируются текстовыми ключами и могут использоваться любыми эле- ментами, находящимися ниже в визуальном дереве. Стили, определенные в сек- ции Resources объекта Application, могут использоваться во всем приложении. В следующем примере определяется панель StackPanel, включающая секцию Resources с определением стиля. Стилю назначается ключ «normal». Три кнопки, дочерних по отношению к StackPanel, обращаются к стилю при помощи расши- рения разметки StaticResource. StyleWithMultipleButtons.xaml <! - - =========================================================== StyleWithMultipleButtons.xaml (с) 2006 by Charles Petzold <StackPanel xml ns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml"> <StackPanel.Resources> <Style x:Key="norma1"> <Setter Property="Control.FontSize" Value="24" /> <Setter Property="Control .Foreground" Value="Blue" /> <Setter Property="Control.HorizontalAlignment" Value="Center" /> <Setter Property="Control.Margin" Value="24" /> <Setter Property="Control .Padding" Valiie="20. 10. 20. 10" /> </Style> </StackPanel.Resources> <Button Style="{StaticResource normal}"> Button Number 1 </Button> <Button Style="{StaticResource normal}"> Button Number 2 </Button> <Button Style="{Stat1cResource normal}"> Button Number 3 </Button> </StackPanel> Поскольку элементы управления и интерфейсные элементы обладают мно- жеством общих свойств, можно определять стили, используемые для разных типов элементов. Следующий стиль используется как элементами управления Button, так и интерфейсным элементом TextBlock. StyleWithMultipleElements.xaml <! - - ============================================================ StyleWithMultipleElements.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml">
662 Глава 24. Стили <StackPanel,Resources> <Style x:Key="normal"> <Setter Property="Control .FontSize" Value="24" /> <Setter Property="Control.Foreground" Value="Blue" /> <Setter Property="Control.HorizontalAlignment" Value="Center" /> <Setter Property="Control.Margin" Value="24" /> <Setter Property="Control.Padding" Value="20. 10, 20, 10" /> </Style> </StackPanel.Resources> <Button Style="{StaticResource normal}"> Button on top of the stack </Button> <TextBlock Style="{StaticResource normal}"> TextBlock in the middle of the stack </TextBlock> <Button Style="{StaticResource normal}"> Button on the bottom of the stack </Button> </StackPanel> Более того, в программе даже можно определить элемент Setter для свойства, уникального для Button: <Setter Property="Button.IsDefault" Value="true" /> Программа все равно будет работать. Элемент TextBlock игнорирует Setter, по- тому что класс TextBlock нс содержит свойства IsDefault. Как и для других ресурсов, один ключ может использоваться в определениях Style нескольких ресурсных секций. Для любого конкретного элемента будет применено определение Style, первым встретившееся при переходе вверх по ви- зуальному дереву. Следующая панель Grid содержит StackPanel с тремя кнопками. Grid опреде- ляет стиль «normal» со свойством FontSize, равным 24, и синим цветом текста (Foreground). Секция Resources панели StackPanel также содержит стиль «normal», у которого свойству Foreground задан красный цвет. StylesWithSameKeys.xaml <i__ ===================================================== StylesWithSameKeys.xaml (с) 2006 by Charles Petzold <Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Grid.Resources> <Style TargetType="{x:Type Button}"> <Setter Property="Control.FontSize" Value="24" /> <Setter Property="Control.Foreground" Value="Blue" />
Стили 663 _____- </Sty1е> </Grid.Resources> <StackPanel> <StackPanel.Resources> <Style TargetType="{x:Type Button}"> <Setter Property="Control.Foreground" Value="Red" /> </Sty1e> </StackPanel.Resources> <Button> Button Number 1 </Button> <Button> Button Number 2 </Button> <Button> Button Number 3 </Button> </StackPanel> </Grid> Кнопки используют стиль, определенный в StackPanel. В них используется красный цвет текста, а свойство FontSize имеет значение по умолчанию — не то, которое определялось в предыдущей секции Style. Использование одного имени для нескольких стилей не позволяет переопределять часть свойств с сохранени- ем предыдущих определений других (вскоре я покажу, как это можно сделать). Выбираются целые определения стилей. Если значение свойства не может быть представлено в виде текстовой строки в Setter, свойство Value необходимо выделить как элемент свойства. В следую- щем XAML-файле используется элемент Setter для свойства Background с гради- ентной кистью. StyleWithPropertyElement.xaml < । _ _ =========================================================== Sty]eWithPropertyElement.xaml (c) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <StackPanel.Resources> <Style x:Key="normal”> Letter Property="Control.FontSize" Value="24" /> Letter Property="Control.HorizontalAlignment" Value=,,Center" /> <Setter Property="Control.Margin" Value="24" /> <Setter Property="Control.Background'^ <Setter.Value> LinearGradientBrush StartPoint="1.0” EndPoint="l,l"> Li nearGradi entBrush.GradientStops>
664 Глава 24. Стили <GradientStop Color="LightBlue" 0ffset="0" /> <Grad1entStop Col01-"Aquamarine" Offset^'!" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Setter.Value> </Setter> </Style> </StackPanel.Resources> <Button Style="{StaticResource normal}"> Button Number 1 </Button> <Button Style="{StaticResource normal}"> Button Number 2 </Button> <Button Style="{StaticResource normal}"> Button Number 3 </Button> </StackPanel> Другое возможное решение — оформить градиентную кисть в виде ресур- са и сослаться на ресурс в определении Style с использованием расширения StaticResource. Как было показано в главе 21, классы Application, FrameworkElement и FrameworkContentElement определяют свойство Resources типа ResourceDictionary. Класс Style тоже определяет свойство Resources, поэтому ресурсы можно размес- тить прямо в определении Style. С позиций архитектуры такой подход пред- почтителен для ресурсов, не используемых в других местах. Пример представлен в следующем XAML-файле. StyleWithResource.xaml <।_ _ ==================================================== StyleWithResource.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <StackPanel.Resources> <Style x:Key=,,normaT,> <Style.Resources> <LinearGradientBrush x:Key="gradbrush" StartPoint="1.0" EndPoint="1.1"> <LinearGradientBrush.GradientStops> <GradientStop Color="LightBlue" Offset="0" /> <GradientStop Color="Aquamarine" Offset="l" /> </L inearGradi entBrush.Gradi entStops>
Стили 665 </LinearGradlentBrush> </Style.Resources^ <Setter Property="Control.FontSize" Value="24" /> <Setter Property="Control.HorizontalAlignment" Value="Center" / <Setter Property="Control.Margin" Value="24" /> <Setter Property="Control.Background" Value="{StaticResource gradbrush}" /> </Sty1e> </StackPanel.Resources> <Button Style="{StaticResource normal}"> Button Number 1 </Button> <Button Style="{StaticResource normal}"> Button Number 2 </Button> <Button Style="{StaticResource normal}"> Button Number 3 </Button> </StackPanel> Класс Style определяет всего шесть свойств; два из них мы уже видели (Setters и Resources). Свойство TargetType позволяет задать тип элемента, к которому дол- жен применяться стиль. В его синтаксисе должно использоваться расширение разметки х:Туре в фигурных скобках, за которым следует имя класса: <Style TargetType="{x:Type Button}" ...> </Style> Расширение x:Type можно рассматривать как XAML-аналог конструкции typeof в С#. Если задано свойство TargetType, указывать х:Кеу не обязательно — ключ генерируется на основании TargetType. Без расширения х:Кеу стиль из пре- дыдущего примера распространяется па все объекты Button элемента, в котором определяется Style, и все объекты Button во всех дочерних элементах. Если это не то, что вам нужно, включите атрибут х:Кеу и укажите его в элементах Button. Если в определении Style используется свойство TargetType, стиль будет при- меняться только к элементам с указанным типом. Например, нельзя использо- вать в TargetType тип Control, чтобы стиль применялся как к элементам Button, так и к элементам Label. Одно из преимуществ TargetType заключается в том, что вы избавляетесь от необходимости полностью уточнять имена свойств в элементах Setter. Обычно используются конструкции следующего вида: <Setter Property="Button.FontSize" Value="24" /> TargetType упрощает разметку до следующего вида: <Setter Property="FontSize" Value="24" />
666 Глава 24. Стили Возможно, из-за этого упрощения атрибут TargetType стоит добавить даже в том случае, если вы собираетесь использовать в стиле атрибут х:Кеу. Кроме того, он прояснит ваши намерения для тех, кому доведется просматривать файл. Следующий XAML-файл определяет два элемента Style: для объектов Button п для объектов TextBlock. Панель StackPanel содержит объект Button с текстовым содержимым, объект TextBlock и объект Button с содержимым TextBlock. Сможете ли вы предсказать размер шрифта и цвет текста этих трех элементов? StylesWithTargetTypes.xaml < । - - =====^==========================™==================== StylesWithTargetTypes.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http: //schemas .microsoft.com/winfx/2006/xanil /presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <StackPanel.Resources> <Style TargetType^’{x:Type Button}"> <Setter Property=,,FontS1ze" Value="24" /> <Setter Property=,,Foreground" Value="Blue" /> </Sty1e> <Style TargetType="{x:Type TextBlock}"> <Setter Property="Foreground" Value=,,Red" /> </Style> </StackPanel.Resources> <Button> Button with Text Content </Button> <TextBlock> TextBlock Text </TextBlock> <Button> <TextBlock> Button with TextBlock Content </TextBlock> </Button> </StackPanel> Первой кнопке назначается стиль с атрибутом TargetType, ссылающимся на класс Button. На кнопке отображается синий текст с размером шрифта 24. Эле- мент TextBlock получает стиль с атрибутом TargetType, ссылающимся на класс TextBlock. Текст выводится красным шрифтом, с сохранением размера шрифта по умолчанию. Нижней кнопке назначается первый стиль — синий текст, раз- мер шрифта 24. Однако объекту TextBlock внутри кнопки назначается второй стиль — с красным цветом текста. Посредством обычного наследования свойств
Стили 667 объект TextBlock наследует свойство FontSize своего родителя Button. На кнопке выводится красный текст с размером шрифта 24. Обычно стиль обладает боль- шим приоритетом, чем наследование свойств, но в стиле TextBlock размер шриф- та не задан, поэтому объект наследует свойство от своего визуального родителя. Если наряду с TargetType задается ключ, то элемент получает конкретный стиль только в том случае, если тип элемента соответствует значению TargetType стиля, а ключи совпадают. У нескольких стилей, определяемых в ресурсной сек- ции, ключи должны быть уникальными. К любому конкретному элементу применяется только один стиль — пер- вый, обнаруженный при поиске по визуальному дереву с совпадающим ключом и/или с классом элемента, совпадающим с TargetType. Иногда требуется опреде- лить новый стиль, основанный па существующем стиле, но обладающий другими или дополнительными определениями свойств. В таких случаях определяется стиль с атрибутом BasedOn, ссылающимся па существующий стиль. В следую- щем примере снова определяется группа из трех кнопок, по обратите внимание: стиль с ключом «hotbtn» определяется с атрибутом Foreground, равным Red. Средняя кнопка использует его вместо стиля «normal». BasedOnStyle.xaml <! -- =============================================== BasedOnStyle.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <StackPanel.Resources> <Style x:Key="normal"> <Setter Property="Control.FontSize" Value="24" /> <Setter Property="Control .Foreground" Value="Blue" /> <Setter Property="Control .HorizontalAlignment" Value="Center" /> <Setter Property="Control.Margin" Value="24" /> <Setter Property="Control.Padding" Value="20, 10. 20. 10" /> </Style> <Style x:Key="hotbtn" BasedOn="{StaticResource normal}"> <Setter Property="Control.Foreground" Value="Red" /> </Style> </StackPanel.Resources> <Button Style="{StaticResource normal}"> Button Number 1 </Button> <Button Style="{StaticResource hotbtn}"> Button Number 2 </Button>
668 Глава 24. Стили <Button Style="{StaticResource normal}"> Button Number 3 </Button> </StackPanel> При определении BasedOn используется такой же синтаксис, который ис- пользуется при указании Style в элементах, и действуют те же правила: новый стиль базируется на первом стиле с совпадающим ключом, обнаруженным в ви- зуальном дереве. Было бы не совсем правильно считать, что вторая кнопка использует свойст- ва, определяемые стилем «normal», за исключением свойств, переопределяемых стилем «hotbtn». Лучше думать, что вторая кнопка использует свойства, опреде- ляемые стилем «hotbtn», тогда как стиль «hotbtn» инициализируется свойствами, изначально определенными для стиля «normal». Элементу ставится в соответст- вие не более одного объекта Style, и для второй кнопки это объект «hotbtn». При написании кода XAML легко забыть, что мы по-прежнему имеем дело со свойствами и объектами. Style — свойство, определяемое классом Framework- Element, по умолчанию оно равно null, а задаваемый этому свойству объект Style содержит только одну коллекцию Setters, в которой хранятся объекты Setter для всех свойств стиля. Стиль также может базироваться на существующем стиле с атрибутом Target- Type, но синтаксис BasedOn немного усложняется: BasedOn="{StaticResource {х:Туре Button}}" В атрибуте TargetType нового стиля должен быть указан класс, на котором базируется стиль, или производный от него. В следующем примере свойства TargetType двух стилей совпадают. BasedOnTargetType.cs <।_ _ =====================^============================== BasedOnTargetType.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml"> <StackPanel.Resources> <Style TargetType="{x:Type Button}"> <Setter Property="Control.FontSize" Value="24" /> <Setter Property="Control.Foreground" Value="Blue" /> <Setter Property="Control.HorizontalAlignment" Value="Center" /> <Setter Property="Control.Margin" Value="24" /> </Sty1e> <Style x:Key="hotbtn" TargetType="{x:Type Button}" BasedOn="{StaticResource {x:Type Button}}"> <Setter Property="Control.Foreground" Value="Red" /> </Style>
669 Стили </StackPanel.Resources> <Button> Button Number 1 </Button> <Button Style="{StaticResource hotbtn}"> Button Number 2 </Button> <Button> Button Number 3 </Button> </StackPanel> В определении второго стиля атрибут х:Кеу должен быть задан явно. В про- тивном случае в программе появятся два конфликтующих стиля. Но попробуйте сделать следующее: замените атрибут TargetType первого сти- ля на Control, атрибут BasedOn второго стиля — тоже на Control, по оставьте атри- бут TargetType второго стиля равным Button. Такая замена допустима. Стиль, объединяющий свойство BasedOn и свойство TargetType, может ссылаться па тот же класс или на производный от него. Теперь второй стиль обладает атрибутом TargetType для класса Button и ключом х:Кеу «hotbtn», поэтому для второй кноп- ки будет использоваться этот стиль. У первой и третьей кнопки стиля не будет. Если исключить определение х:Кеу из второго стиля и атрибут Style из второй кнопки, всем трем кнопкам будет назначен второй стиль. Если вы используете TargetType, чтобы определенным типам элементов все- гда назначались определенные стили, появляется возможность определения иерархии стилей, воспроизводящей иерархию классов. Пример: TargetTypeDerivatives.xaml < I _ _ === = = = = = = = = = = = = = = = = = = = = = = = =: = = = = = = = = = = = = = = = = = = = = = = = =;= = = = = TargetTypeDerivatives.xaml (с) 2006 by Charles Petzold <StackPanel xmlns=”http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.mi crosoft com/wi nfx/2006/xaml"> <StackPanel.Resources> <Style TargetType="{x:Type Control}”> <Setter Property="Control.FontSize" Value="24" /> <Setter Property="Control.Foreground" Value="Blue" /> <Setter Property="Control.HorizontalAlignment" Value="Center" /> <Setter Property="Control.Margin" Value="24" /> </Sty1e> <Style TargetType="{x:Type Button}" BasedOn="{StaticResource {x:Type Control}}">
670 Глава 24, Стили <Setter Property="Control .Foreground" Value="Red" /> </Style> <Style TargetType="{x:Type Label}" BasedOn="{StaticResource {x:Type Control}}"> <Setter Property="Control.Foreground" Value="Green" /> </Sty1e> <Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource {x:Type Control}}"> </Style> </StackPanel.Resources> <Button> Button Control </Button> <Label> Label Control </Label> <TextBox> TextBox Control </TextBox> </StackPanel> Последний элемент Style объекта TextBox не определяет никаких новых свойств, поэтому объекту TextBox назначается синий цвет текста. Конечно, последующие элементы Style могут определять свойства, специфические для конкретных ти- пов элементов. Стилям присущи некоторые изначальные ограничения. Допустим, вы хотите определить стиль для объектов StackPanel, чтобы они всегда содержали несколь- ко элементов одного типа: <!-- Не работает ! --> <Style TargetType="{x:Type StackPanel}"> <Setter Property="Children"> <Setter.Value> </Setter.Value> </Setter> </Style> Главная проблема заключается в том, что свойство Children, определяемое классом Panel, не поддерживается зависимым свойством; следовательно, для за- дания этого свойства не могут использоваться стили. Возможно, для решения подобных задач придется определить новый класс, производный от StackPanel, или шаблон (см. следующую главу).
Стили 671 Следующий фрагмент XAML пытается определить стиль для кнопок, чтобы они всегда содержали объект Image: <!-- Не работает ! --> <Style TargetType="{x:Type Button}"> ^Setter Property="Content"> <Setter.Value> <Image ... /> </Setter.Value> </Setter> </Style> Как видно из сообщения об ошибке, проблема заключается в том, что «Setter не поддерживает значения, производные от Visual или ContentElement». Любые объекты, упоминаемые в определении Style, создаются однократно как часть стиля, а следовательно, — совместно используются всеми элементами, исполь- зующими стиль. Элемент Image (а в действительности любой элемент, произ- водный от Visual пли ContentElement) может иметь только одного родителя, а при совместном использовании объекта Style это условие было бы нарушено. При возникновении подобных проблем рассмотрите возможность использования спе- циального класса или шаблона. Как показывает следующий фрагмент XAML, атрибут Value элемента Setter может содержать привязку данных. SetterWithBinding.xaml <1 __ ==================================================== SetterWithBinding.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <StackPanel.Resources> <Style TargetType="{x:Type Button}"> <Setter Property="FontSize" Value="{Binding ElementName=scrol1. Path=Value}" /> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="Margin" Value="24" /> </Sty1e> </StackPanel.Resources> <ScrollBar Name="scroll" Orientation=,,Horizontal" Margin="24" Min1mum="H" Maximum=,,100" Value="24" /> <Button> Button Number 1 </Button> <Button>
672 Глава 24. Стили Button Number 2 </Button> <Button> Button Number 3 </Button> </StackPanel> Элемент Style объекта Button содержит элемент Setter для свойства FontSize, у которого атрибут Value привязывается к свойству Value объекта ScrollBar с име- нем scroll. При перемещении бегунка кнопки увеличиваются или уменьшаются. Мы рассмотрели применение стилей с простыми элементами управления и объектами TextBlock, но стили также играют важную роль при выводе графи- ки. В следующем XAML-файле определяется стиль, у которого атрибут Target- Type ссылается на тип Ellipse. Стиль определяет контуры и размеры всех эллип- сов, а местоположение и цвет фигуры задаются на уровне каждого конкретного эллипса. GraphicsStyles.xaml <।_ _ ================================================= GraphicsStyles.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Canvas.Resources> <Style TargetType="{x:Type El 1ipse}"> <Setter Property="Stroke" Value="Black" /> <Setter Property=,,StrokeThickness" Value=,,3" /> <Setter Property="Width" Value="96" /> <Setter Property="Height" Value=,,96" /> </Style> </Canvas.Resources> <E11 ipse Canvas.Left="100" <E11 ipse Canvas.Left="150" <E11 ipse Canvas.Left="200" <E11 ipse Canvas.Left="250" <E11 ipse Canvas.Left="300" /Canvas> Canvas.Top="50" El 11 -"Blue" /-> Canvas.Top="100" Fill="Red" Л Canvas.Top="150" Fill="Green" /> Canvas.Top-"100" Fill="Cyan" /> Canvas.Top="50" Fill-"Magenta" !' Предположим, требуется нарисовать серию горизонтальных линий (как, на- пример, в программе YellowPad из главы 22). Конечно, элемент Style должен содержать элемент Setter для свойства Stroke, а поскольку начало и конец всех горизонтальных линий имеют одинаковые координаты X, в определение стиля также будут включены элементы Setter для XI и Х2. <Style TargetType="Line"> <Setter Property="Stroke" Value="Blue" />
673 Стили <Setter Property="X1" Value-"100" /> <Setter Property="X2" Value=,,300" /> </Style> В этом случае отдельные элементы Line выглядят совсем просто: <Line Yl="100" Y2-"100" /> <L1ne Yl="125" Y2="125" /> <Line Yl="150" Y2="150" /> Все это, конечно, хорошо. Но если вам когда-нибудь потребуется изменить местоположение липни (скажем, сместить их вниз на 12 единиц), придется вручную изменять значения Y1 и Y2, то есть фактически выполнять всю работу дважды. Раз линии являются горизонтальными, а значения Y1 и Y2 всегда совпа- дают, нельзя ли задать значение в каждом элементе Line 'только одни раз? Проблема решается конструкцией Binding с использованием атрибута Relative- Source, описанного в конце предыдущей ктавы. Для свойства Y2 можно опреде- лить элемент Setter, который указывает, что его значение совпадает со значени- ем свойства Y1: <Setter Property="Y2" Value-"{Binding RelalivoSource-{RelativeSource self}, Patli-Yl}" /> Теперь в отдельных элементах Line указывается только значение Y1, которое автоматически используется в качестве значения Y2. В следующем XAML-файле эта методика используется для рисования сетки из горизонтальных и вертикальных линий. Оба стиля, horz и veil, основаны на стиле с ключом «base», определяющем цвета линий. DrawGrid.xaml < I _- =========================================== DrawGrid.xaml (с) 2006 by Charles Petzold =========================================== „> <Canvas xmlns="http://schemas.Microsoft.com/wnitx/2006/xaml/piesentation" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Canvas.Resources> <Style x:Key="base" TargetType="Line"> <Setter Property="Stroke" Value-"Blue" /> </Sty1e> <Style x:Key-"horz" Target!ype="Line" BasedOn-"{StaticResource base}"> <Setter Property="Xl" Value-"100" /> <Setter Property="X2" Value="300" /> <Setter Property="Y2" Value-"{Binding RelativeSource={ReIativeSource self}, Path=Yl}" /> 7Style>
674 Глава 24. Стили <Style x:Key="vert" TargetType="Line" BasedOn="{Stat1cResource base}"> <Setter Property="Yl" Value="100M /> <Setter Property="Y2" Value="300" /> <Setter Property="X2" Value="{Binding RelativeSource={RelativeSource self}, Path=Xl}" /> </Style> </Canvas.Resources> <L1ne Style="{StaticResource horz}" Yl="100" /> <Line Style="{StaticResource horz}" Yl="125" /> <Line Style="{StaticResource horz}" Yl="150" /> <Line Style="{StaticResource horz}" Yl="175" /> <Line Style="{StaticResource horz}" Yl="200" /> <Line Style="{StaticResource horz}" Yl="225" /> <Line Style="{StaticResource horz}" Yl="250" /> <Line Style="{StaticResource horz}" Yl="275" /> <Line Style="{StaticResource horz}" Yl="300" /> <Line Style="{StaticResource vert}" Xl="100" /> <Line Style="{StaticResource vert}" Xl="125" /> <Line Style="{StaticResource vert}" Xl="150" /> <Line Style=”{StaticResource vert}" Xl="175" /> <Line Style="{StaticResource vert}" Xl="200" /> <Line Style="{StaticResource vert}" Xl="225" /> <Line Style="{StaticResource vert}" Xl="250" /> <Line Style="{StaticResource vert}" Xl="275" /> <Line Style="{StaticResource vert}" Xl="300" /> </Canvas> На самом деле для вывода повторяющихся элементов следовало бы приме- нить цикл for в коде С#, однако повторение чисел в отдельных элементах сокра- тилось до такой степени, что применение XAML становится почти оправданным. Свойство Style определяется не только для FrameworkElement, но и для Frame- workContentElement; это означает, что стили можно определять для классов, про- изводных от TextElement. Следовательно, мы можем определять стили, исполь- зуемые при форматировании текста в элементах FlowDocument. Вот как выглядит первый абзац «Алисы в стране чудес» с определениями двух стилей абзацев. DocumentStyles.xaml <।_ _ ================================================= DocumentStyles.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Title="I. Down the Rabbit-Hole"> <Page.Resources>
Стили 675 <Style TargetType="{x:Type Paragraph}" x:Key=',Normar,> <Setter Property="TextIndent" Value="0.25in" /> </Style> <Style TargetType="{x:Type Paragraph}" x:Key="ChapterHead"> <Setter Property="TextAlignment" Value="Center" /> <Setter Property="FontSize" Value="16pt" /> </Style> </Page.Resources> <F1owDocumentReader> <FlowDocument> Paragraph Style="{StaticResource ChapterHead}"> Chapter I </Paragraph> Paragraph Style="{StaticResource ChapterHead}"> Down the Rabbit-Hole </Paragraph> Paragraph Style="{StaticResource Normal}"> Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do: once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations In It. &#x201C;and what is the use of a book,&#x201D: thought Alice. &#x201C;without pictures or conversations?&#x201D; </Paragraph> Paragraph Style="{StaticResource Normal}"> </Paragraph> </FlowDocument> </F1owDocumentReader> </Page> Конечно, мне не нужно объяснять, насколько полезны текстовые стили и воз- можность определять один стиль на основе другого. На этом принципе основа- ны целые технологии (например, CSS). Хотя самым распространенным дочерним элементом Style является Setter, вы также можете использовать элемент EventSetter для назначения обработчика маршрутизируемого события — еще один способ совместного использования обработчиков событий несколькими элементами. Проект EventSetterDemo содер- жит как XAML-файл, так и файл С#. XAML-файл, приведенный ниже, содержит секцию Style с элементом EventSetter. EventSetterDemo.xaml <! -. ================================================== EventSetterDemo.xaml (с) 2006 by Charles Petzold
676 Глава 24. Стили <Wi ndow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Petzold.EventSetterDemo.EventSetterDemo" Title="EventSetter Demo"> <Window.Resources> <Style TargetType="{x;Type Button}"> <Setter Property=,,FontSize" Va 1 ue="24" /> <Setter Property^'HorizontalAlignment" Value^'Center" /> <Setter Property="Margin" Value^'24" /> <EventSetter Event="Click" Handler=,,ButtonOnClick" /> </Sty1e> </Window.Resources> <StackPanel> <Button> Button Number 1 </Button> <Button> Button Number 2 </Button> <Button> Button Number 3 </Button> </StackPanel> </Window> Обработчик события, упоминаемый в элементе Eventsetter, определяется в час- ти класса, написанной на С#. EventSetterDemo.cs //-------------------------------------------------- // EventSet.terDemo.cs (с) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Windows; us1 ng System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.EventSetterDemo public partial class EventSetterDemo : Window [STAThread] public static void Main() Application app = new Application(); app.Run(new EventSetterDemo());
677 Стили } public EventSetterDemo() { InitializeComponent(): } void Button0nC11ck(object sender, RoutedEventArgs args) { Button btn = args.Source as Button: MessageBox.Show("The button labeled " + btn.Content + " has been clicked". Title): } Как уже говорилось ранее, класс Style определяет шесть свойств. Мы рассмот- рели свойства Setters, Resources, TargetType и BasedOn. Свойство IsSealed, доступное только для чтения, становится истинным при вызове метода Seal, что происхо- дит при использовании стиля. Последнее свойство Triggers позволяет управлять реакцией других элементов на изменения свойств элемента, изменения в при- вязке данных или на события. При первом знакомстве с элементами Setter и Trigger легко запутаться. В обо- их случаях задаются значения свойств, но Setter задает свойства при создании элемента, a Trigger — при выполнении некоторых условий. Свойство Triggers элемента Style относится к типу Triggercollection (коллек- ция объектов типа TriggerBase). Классы, производные от абстрактного класса TriggerBase, представлены на следующей схеме: Object DI spatcherObject (абстрактный) Dependencyobject TriggerBase (абстрактный) DataTrlgger EventTrigger MultiDataTrlgger MuitITrigger Trigger В этой главе не рассматривается класс EventTrigger, инициирующий измене- ния в элементе при возникновении заданного события. Мы вернемся к этому классу в главе 30. На практике из всех потомков TriggerBase чаще всего применяется Trigger, оп- ределяющий реакцию элемента управления пли интерфейсного элемента на из- менение некоторого свойства. Очень часто такие свойства зависят от действий пользователя (например. IsMouseOver). Триггер реагирует, изменяя другое свой- ство через определение Setter. Типичное определение Trigger в элементе Style вы- глядит примерно так: <Style.Triggers> «Trigger Property="Control.IsMouseOver" Value="True">
678 Глава 24. Стили <Setter Property="Control.Fontstyle" Value=“Italic" /> <Setter Property="Control.Foreground" Value="Blue" /> </Tr1gger> </Style.Tr1ggers> Обратите внимание: в определении стиля должен использоваться элемент свойства Style.Triggers. Атрибуты Property и Value элемента Trigger указывают, что триггер срабатывает в тот момент, когда свойство IsMouseOver становится ис- тинным. Триггеры не ограничиваются логическими свойствами, но, конечно, эти свойства применяются чаще других. Свойство, указанное в элементе Trigger, должно поддерживаться зависимым свойством. Как и Style, класс Trigger содержит свойство с именем Setters; это свойство тоже является свойством содержимого Trigger, и объект может содержать несколько дочерних элементов Setter. Два объекта Setter в приведенном примере означают, что при наведении указателя мыши на элемент текст последнего должен выво- диться синим курсивным шрифтом. Пример полного XAML-файла с двумя элементами Trigger. StyleWithTriggers.xaml <I_ _ ==================================================== StyleWithTriggers.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xamI/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml"> <StackPanel.Resources> <Style x:Key="norma 1"> <Setter Property="Control.FontSize" Value="24" /> <Setter Property="Control.HorizontalAlignment" Value="Center" /> <Setter Property="Control.Margin" Value="24" /> <Style.Tr1ggers> <Tr1gger Property="Control.IsMouseOver" Value="true"> <Setter Property="Control.Fontstyle" Value="Italic" /> <Setter Property="Control.Foreground" Value="Blue" /> </Tr1gger> <Tr1gger Property=“Button.IsPressed" Value="true"> <Setter Property="Control.Foreground" Value="Red" /> </Tr1gger> </Style.Tr1ggers> </Style> </StackPanel.Resources> <Button Style="{Stat1cResource normal}"> Button Number 1 </Button> <Button Style="{StaticResource normal}"> Button Number 2
Стили 679 </Button> <Button Style="{Stat1cResource normal}"> Button Number 3 </Button> </StackPanel> Другой элемент Trigger изменяет цвет текста в том случае, когда свойство IsPressed принимает истинное значение. В данном случае порядок следования элементов Trigger влияет на работу триггеров. Если последовательные элементы Trigger задают одни и те же свойства, то поздние триггеры переопределяют дей- ствие ранних. Если переставить местами два свойства Trigger в приведенном XAML-файле, текст никогда не будет окрашиваться в красный цвет, потому что свойство IsPressed истинно только при истинном свойстве IsMouseOver, а триггер IsMouseOver задает синий цвет текста. Если вам не удается подобрать правильный порядок триггеров, воспользуй- тесь элементом MultiTrigger. Он аналогичен Trigger, но вступает в действие при одновременном выполнении двух и более условий. Пример: MultiTriggerDemo.xaml < । _ _ =================================================== MultiTriggerDemo.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml"> <StackPanel.Resources> <Style x:Key="norma1"> <Setter Property="Control.FontSize" Value=,,24" /> <Setter Property="Control.HorizontalAlignment" Value="Center" /> <Setter Property="Control.Margin" Value=,,24" /> <Style.Tr1ggers> <Tr1gger Property="Button.IsPressed" Value="True"> <Setter Property="Control.Foreground" Value="Red" /> </Tr1gger> <Mult1Tr1gger> <Mu1tiTrigger.Conditions> <Cond1t1on Property="Control.IsMouseOver" Value="True" /> <Cond1t1on Property="Button.IsPressed" Value="False" /> </Mul11T rlgger.Condi11ons> <Setter Property="Control.Fontstyle" Value="Ital1c" /> <Setter Property="Control.Foreground" Value="Blue" /> </Mult1Tr1gger>
680 Глава 24. Стили </Sty1е.Triggers> </Sty1e> /StackPanel.Resources> <Button Style="(StaticResource normal}"> Button Number 1 </Button> <Button Style="{StaticResource normal}"> Button Number 2 </Button> <Button Style="{StaticResource normal}"> Button Number 3 </Button> </StackPanel> Свойство Conditions класса MultiTrigger содержит объект типа Conditioncollection. Каждый элемент Condition описывает свойство (Property) и значение (Value). Текст на кнопке выводится синим цветом, когда указатель мыши находится над кноп- кой, и красным, когда на кнопке делается щелчок. Текст выводится курсивом только тогда, когда указа гель мыши находится нац кнопкой, но кнопка не нажа- та, потому что этот вариант оформления указан в части MultiTrigger с указанны- ми условиями. Класс DataTrigger в общем похож па Trigger, но в нем Property заменяется па Binding. В общем случае Binding ссылается па другой элемент. Как показывает следующий файл, DataTrigger может задавать свойство, когда Binding принимает определенное значение. StyleWithDataTrigger.xaml <1 __ ^ = = = = = = = = = = = = = = = ^ = ==^ = = ^ = = = = = = = = = = = =; = = = = = = = = ^ = = ^ = =;=. = : StyleWlthDatafi Igger.xaml (с) 2006 by Charles Petzold <StackPanel xmIns="http://schemas.m1crosofc.com/w1nrx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml"> <StackPanel .Resources'’ <Style TargetType="{x:Type Button}"> <Setter Property="FontS1ze" Value="24" /> <Setter Property="Horizontal AlIgnment" Value="Center" /> <Setter Property="Margin" Value="24" /> <Style.Tr1ggers> <DataTrigger Bind1ng="{B1nd1ng ElementName=txtbox, Path-Text.Length}" Value="0,,> <Settcr Property="IsEnabled" Value="False" /> </DataTr1gger> </Style.Triggers>
Стили 681 </Style> </StackPanel.Resources> <TextBox Name="txtbox" HorizontalAlignment="Center" Width="2in" Margin="24" /> <Button> Button Number 1 </Button> <Button> Button Number 2 </Button> <Button> Button Number 3 </Button> </StackPanel> Элемент DataTrigger содержит привязку к элементу TextBox, на который он ссылается по имени txtbox. Атрибут Path ссылается иа свойство Length свойства Text объекта TextBox. Когда его значение равно 0, свойство IsEnabled кнопки за- дается равным false. В результате кнопка доступна лишь при наличии текста в TextBox. Диалоговые окна часто блокируют кнопку ОК до выполнения некото- рых условий; эта задача идеально подходит для DataTrigger. MultiDataTrigger играет ио отношению к DataTrigger ту же роль, что и MultiTrigger для Trigger. Свойства задаются только при выполнении всех условий привяз- ки. Как и MultiTrigger, MultiDataTrigger содержит один или несколько элементов Condition. Класс Condition определяет свойства Property, Binding и Value; создается впечатление, что обычные триггеры могут объединяться с триггерами данных, но это не так. При определении MultiTrigger используются свойства Property и Value класса Condition, а при определении MultiDataTrigger — свойства Binding и Value. В следующем XAML-файле кнопки становятся доступными только в том слу- чае, если установлены оба элемента CheckBox. MultiDataT riggerDemo.xaml <i, _ ====sss==s=======ss=s======================s====ss=:==== MuitlDataTrlggerDemo.xaml (c) 2006 by Charles Petzold <StackPanel xml ns="http: //schemas .mi crosoft .com/v/inf x/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <StackPanel.Resources> <Style TargetType="{x:Type CheckBox}"> <Setter Property="HorizontaIA1 ignment" Value=,,Center" /> <Setter Property=,,Margin" Value="12" /> </Style> <Style TargetType="{x:Type Button}"> <Setter Property="FontSize" Value="24" />
682 Глава 24. Стили <Setter Property="Hor1zontalAlignment" Value="Center" /> <Setter Property="Margin" Value="12" /> <Setter Property="IsEnabled" Value="False" /> <Style.Triggers> <MultiDataTrigger> <Mu111DataTrigger.Condi ti ons> Condition B1nding="{B1nding ElementName=chkboxl. Path=IsChecked}" Value="True" /> Condition Binding="{Binding ElementName=chkbox2. Path=IsChecked}" Value="True" /> </MultiDataTrigger.Conditions> <Setter Property="IsEnabled" Value="True" /> </MultiDataTrigger> </Style.Triggers> </Style> </StackPanel.Resources> CheckBox Name="chkboxl,,> Check 1 </CheckBox> CheckBox Name="chkbox2"> Check 2 </CheckBox> <Button> Button Number 1 </Button> <Button> Button Number 2 </Button> <Button> Button Number 3 </Button> </StackPanel> Итак, стили являются удобным средством для упорядочения оформления элементов. Простейший стиль занимает несколько строк в XAML-файле и со- кращает объем повторяющегося кода между сходными элементами, тогда как сложные стили могут занимать целые файлы и обеспечивать единство оформ- ление всех элементов в приложениях. Каждый раз, когда потребуется задать двум элементам одинаковые значения свойств, воспользуйтесь стилем — и вы не пожалеете об этом, если вам когда-либо придется изменить эти значения.
25 Шаблоны Некоторые программы в первой части книги дают начальное представление о шаблонах. Так, проект BuildButtonFactory в главе 11 показывает, как создать объект типа ControlTemplate и задать его свойству Template объекта Button. По сути тот объект ControlTemplate представлял собой полное описание внешнего вида кнопки, а также его изменения при модификации некоторых свойств (таких, как IsMouseOver и IsPressed). Вся логика кнопки, а также весь механизм обработ- ки событий оставались без изменений. ControlTemplate составляет важную подгруппу шаблонов, поддерживаемых в WPF. Как подсказывает название, ControlTemplate используется для определе- ния внешнего вида элементов. Класс Control обладает свойством Template, кото- рому и задается объект ControlTemplate. На первый взгляд кажется, что стили и шаблоны решают похожие задачи, но это не так. Элемент управления или интерфейсный элемент не обладает свойст- вом Style по умолчанию, поэтому свойство Style обычно равно null. Оно использу- ется для определения задаваемых свойств и триггеров, связываемых с элементом. С другой стороны, все элементы управления с визуальным оформлением, оп- ределяемые в WPF, изначально обладают свойством Template, которому задается объект типа ControlTemplate. Кнопка выглядит как кнопка, а полоса прокрутки — как полоса прокрутки, и это является прямым результатом работы объектов ControlTemplate. Объект ControlTemplate определяет все визуальное оформление элемента, но при желании вы можете его заменить. Таким образом, внешний вид элемента WPF не связан неразрывно с функциональностью элемента. В проекте ListColorsEvenElegantlier главы 13 был представлен объект Data- Template. Класс ItemsControl (от которого наследует ListBox) определяет свойство ItemTemplate; задавая ему объект DataTemplate, можно управлять отображением каждого элемента в списке ListBox. В программе ListColorsEvenElegantlier этот объект определял визуальное дерево с элементами Rectangle и объектами TextBlock, обеспечивающее вывод цветовых образцов в ListBox. В главе 13 также были представлены специализированные элементы Color- GridBox и ColorWheel, которые были объявлены производными от ListBox, но пред- ставляли данные в совершенно ином формате. Это стало возможно благодаря
684 Глава 25. Шаблоны свойству ItemsPanel класса ItemsControl, которому задавался объект типа Items- PanelTemplate. Как подсказывает название, этот объект определяет тип панели, используемый для отображения данных в объектах ListBox. Шаблоны также встречались в некоторых проектах главы 16. Как в проекте ListSortedSystemParameters, так и в элементе DependencyPropertyListView исполь- зовались элементы ListView, причем в обоих случаях свойству View элемента ListView задавался объект GridView. Объект GridView обеспечивает вывод объектов по столбцам в элементе ListView. Если содержимое столбца вас почему-либо не устраивает, вы можете создать объект DataTemplate, определяющий внешний вид объектов в этом столбце, и задать его свойству CellTemplate объекта GridViewColumn. Как видно из следующей иерархии классов, все разновидности шаблонов яв- ляются производными от абстрактного класса FrameworkTemplate: Object FrameworkTemplate (абстрактный) ControlTemplate DataTemplate HI erarchi cal DataTemplale ItemsPanelTemplate Конечно, вы начнете различать шаблоны (и свойства, которым они задают- ся) лишь по мере пакой тения практического опыта. Далее приводится краткая сводка. Объекты тина ControlTemplate создаются для назначения свойству Template, оп- ределяемому классом Control. Они определяют все визуальное оформление эле- мента управления, включая его изменения, инициируемые внешними условиями. С одной стороны, использование свойства Template открывает большие возмож- ности, с другой — сопряжено с немалой ответственностью. Объекты типа ItemsPanelTemplate задаются свойству ItemsPanel. определяемо- му классом ItemsControl. Они описывают тип панели, используемой для отобра- жения нескольких логических элементов в элементах ItemsControl (например, ListBox пли ComboBox), и составляют простейшую разновидность шаблонов. Все остальные шаблоны относятся к типу DataTemplate. Несомненно, имен- но они образуют группу шаблонов, чаще всего применяемых на практике. Объ- екты DataTemplate присутствуют во всех элементах, производных от Contentcontrol и ItemsControl, и позволяют управлять отображением содержимого и логических элементов списков. Абстрактный класс FrameworkTemplate определяет всего три свойства. Логи- ческое свойство IsSealed, доступное только для чтения, указывает, возможно ли изменение шаблона. Свойство Resources типа ResourceDictionary предназначено для определения ресурсов, доступных только внутри шаблона. Самое важное из трех свойств, VisualTree, определяет расположение компонентов, образующих визуаль- ное оформление элемента управления (пли содержимого элемента управления, пли логических элементов списка). Свойство VisualTree относится к типу FrameworkElementFactory. Припомните проекты с шаблонами в части 1 книги, и вы увидите, насколько странным и не- удобным был класс FrameworkElementFactory, позволяющий определять иерархии
Шаблоны 685 элементов (включая свойства и триггеры) без фактического создания этих эле- ментов. Полученный программный код выглядел весьма непривлекательно. При определении шаблонов в XAML FrameworkElementFactory выполняет всю работу «за кулисами». Синтаксис XAML, используемый при определении шабло- нов, не отличается от синтаксиса определения макетов элементов и их свойств, если не считать того, что такие элементы создаются лишь при возникновении необходимости. Класс ControlTemplate дополняет набор свойств, определяемым в Framework- Template, еще двумя свойствами. TargetType указывает тип элемента, для которого предназначен шаблон; Triggers представляет собой коллекцию объектов Trigger. Следующий автономный XAML-файл весьма близок к программе BuildButton- Factory из главы И, не считая того, что он не связывает с кнопкой обработчик события Click. ButtonWithTemplate.xaml ButtonWithtempiate.xaml (c) 2006 by Charles Petzold <Page xmIns="http://schemas.microsoft.com/winfx/2006/xami/presentation" xmlns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml"> <Button HorizontalAl ignment="Center" Vert 1 cal Al igriment="Center" FontSize="48" Padding="20"^ Button with Custom Template <Button.Tempiate> <Control Template-' < !-- Свойство VisualTree объекта ControlTemplate --> <Border Name="border" Borderlhickness="3" BorderBrush="Red" Background-"(DynamicResource {x:Static SystemColors.Control LightBrushKey}}"> <TextBlock Name="txtblk" FontStyle="Italic" Text="{TemplateBinding Contentcontrol.Content}" Margin="{TemplateBinding Control.Padding}" /> </Border> < !-- Свойство Triggers объекта ControlTemplate --> < ControlTemplate.Tri ggers> <Trigger Property-"UIElement.IsMouseOver" Value="True"> <Setter TargetName-"border" Pronerty="Border.CornerRadius" Vaiue="24" /> <Setter FargetName=-"txtblk" Property="TextBlock.FontWeight" Value="Bold" /> </Trigger> <1 rigger Property="Button.IsPressed" Vaiue="True">
686 Глава 25. Шаблоны <Setter TargetName="border" Property="Border.Background" Vaiue="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" /> </Trigger> </ControlTempi ate.Triggers> </ControlTempiate> </Button.Tempi ate> </Button> </Page> В начале определения Button обычно определяются некоторые свойства (Hor- izontalAlignment, VerticalAlignment, FontSize и Padding) и содержимое кнопки — в дан- ном случае это текстовая строка «Button with Custom Template». Элемент свойства Button.Template заключает определение свойства Template кнопки. Это свойство относится к типу ControlTemplate. Свойством содержимого для класса FrameworkTemplate и всех его потомков является VisualTree, поэтому описание макета элемента может начинаться сразу же после начального тега ControlTemplate. Все начинается с одного элемента верхнего уровня; остальные элементы являются дочерними по отношению к нему. Очень часто элемент VisualTree начинается с элемента Border. Так, в файле ButtonWithTemplate.xaml этот элемент задает толщину рамки в 3 единицы и красный цвет. Свойство Back- ground определяется системным цветом, для получения которого используется расширение разметки DynamicResource. Внутри элемента Border находится элемент TextBlock с атрибутом Fontstyle, инициализированным значением Italic. Использование TextBlock косвенно ограничивает кнопку выводом текста. Свой- ство Text объекта TextBlock задается свойству Content объекта Button. Расширение разметки TemplateBinding определяется специально для связывания свойства в ви- зуальном дереве шаблона со свойством, определяемым элементом. Синтаксис привязки свойства Text объекта TextBlock к свойству Content объекта Button вы- глядит так: Text="{TemplateBinding Contentcontrol.Content}" Аргумент TemplateBinding является зависимым свойством, поэтому обычно он включает класс, в котором определяется свойство, или класс, унаследовавший его. Вместо Contentcontrol.Content здесь можно использовать Button.Content, про- грамма работала бы точно так же. К счастью, логика шаблонов WPF достаточно гибка, чтобы свойство типа string могло объединяться посредством TemplateBinding со свойством типа object. Следующий атрибут элемента TextBlock выглядит интереснее: Margin="{TemplateBinding Control.Padding}" Внешние отступы TextBlock (то есть пространство между TextBlock и Border) связываются со свойством Padding элемента Button, то есть внутренним свободным пространством, окружающим содержимое кнопки. Подобные привязки Template-
Шаблоны 687 Binding типичны для Contentcontrol. Конечно, сам элемент Button по-прежнему об- ладает собственным свойством Margin, определяющим величину пространства вокруг кнопки. После определения VisualTree дополнительный элемент свойства ControlTemplate. Triggers определяет триггеры. В этом конкретном шаблоне определяются два триг- гера — для свойств IsMouseOver и IsPressed. В обоих случаях триггер активизиру- ется при переходе свойства в истинное состояние. В тег ControlTemplate также можно включить атрибут TargetType: <ControlTempi ate TargetType="{x:Type Button}"> В этом случае перед зависимыми свойствами в выражениях TemplateBinding не нужно ставить имена классов, потому что привязка содержит всю информа- цию, необходимую для ссылок на свойства. Как показывает программа ButtonWithTemplate.xaml, элемент ControlTemplate можно определить и для одного элемента. Но на практике шаблоны обычно со- вместно используются несколькими элементами, и один из способов основан на их определении в виде ресурсов. Далее приводится автономный XAML-файл с элементом ControlTemplate, определяемым в виде ресурса с ключом «btnCustom». Использование атрибута TargetType позволило обойтись без указания имен клас- сов перед зависимыми свойствами и маршрутизируемыми событиями. ResourceTemplate.xaml ResourceTemplate.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Page.Resources> <ControlTempi ate x:Key="btnCustom" TargetType="{x:Type Button}"> <!-- Свойство VisualTree объекта ControlTemplate --> <Border Name="border" BorderThickness="3" BorderBrush="Red" Background^'{TempiateBinding Foreground}"> <TextBlock Name="txtblk" FontStyle="Italic" Text="{TempiateBinding Content}" Margin="{TempiateBinding Padding}" Foreground="{TemplateBinding Background}" /> </Border> <!-- Свойство Triggers объекта ControlTemplate --> <ControlTempi ate.Tri ggers> <Trigger Property="IsMouseOver" Value="True"> <Setter TargetName="border" Property="CornerRadius" Value="12" /> <Setter TargetName="txtblk" Property="FontWeight" Value="Bold" /> </Trigger>
688 Глава 25. Шаблоны <Trigger Property="IsPressed” Value="True"> <Setter Tai'getNanie="border" Property-"Background" Vaiue="{Bind!ng Path=Background}" /> <Setter TargetName="t/tblk" Prcperty="Foreground" Va1ue="{Binding Path=Foreground}" /> </lrigger> </ControlTempi ate.Triggers> </ControlTempiate> </Page. Resoi и ces> <StackPane> <Button Template="{StaticRcsource btnCustom}" HorizontalAlignment="Center" Margin^"24" FontSize="24" Padding="10" > Button with Custom Template </Button> <Button Horizonta 1 Al igninent="Center" Margiir"24" Fontsize="24" Fadding="ld" -• Normal Button </Button> <Button Template" {StaticResource btnCustom)" HorizontalAl iqnmcnt=-"Ceiiter" Margin="24" FontSize="24" Padding--"10" > Another Button with Custom Template </BuLt.on> </StackPaneb </Page> Ближе к концу файла располагается определение напели StackPanel с тремя кнопками. Все кнопки обладают одинаковыми свойствами, кроме того, что у пер- вой и третьей кнопки свойству Template задается ресурс. Этот конкретный шаб- лон меняет местами цвет фона и цвет текста. Скажем, если в системе выбран бе- лый цвет фона и черный текст, .эти две кнопки будут выводить белый текст на черном фоне, а при нажатии кнопок цвета будут переключаться. Вряд ли вам понадобится проделывать что-нибудь подобное, но пример даст представление о гибкости шаблонов. Style и ControlTemplate могут объединяться в одном ресурсе. Элемент Style начи- нается, как обычно, а затем свойству Template задается объект тина ControlTemplate: <Style ... > <Setter Property="Template"> <Setter.Value> kontrolTemplate .. > </Controltemp lai e> </Setter.Value> </Setter> </Sty1e>
Шаблоны 689 ------ В этом варианте как стиль, так и шаблон элемента назначаются простым за- данием свойства Style. Первые два шаблона, представленные в этой главе, основаны на возмутитель- ном допущении, что свойству Content объекта Button задается текстовая строка. Если это условие не выполняется, шаблоны выдают ошибки времени выполне- ния. Даже если визуальное оформление кнопок будет полностью переработано, вряд ли вы захотите ограничиваться выводом простого текста. Все классы, производные от Contentcontrol, используют объект типа Content- Presenter для вывода своего содержимого. Класс ContentPresenter является произ- водным от FrameworkElement, а объект ContentPresenter может включаться в визу- альное дерево шаблона. Такое решение гораздо лучше, чем предположение о том, что содержимое всегда является текстом. Файл CircledRadioButtons.xaml определяет ресурс ControlTemplate для объектов типа RadioButton. Обратите внимание па пустой элемент ContentPresenter в эле- менте Border. CircledRadioButtons.xaml < I - - ====== = = = ^ ===== = ============================ ======:=== = CireledRadioButtons.xaml (c) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Page.Resources> Control Tempi ate x:Key="newradio" TargetType="{x:Type RadioButton}"> <Border Name="border" BorderBrush="(DynamicResource {x:Stati c SystemColors.ControlTextBrushKey}}" Padding-"10" CornerRadius="100"> <ContentPresenter /> </Border> <ControlTempi ate.Tri ggers> <Trigger Property="IsChecked" Value="True"> <Setter TargetName="border" Property="BorderThickness" Value="l" /> </Trigger> </ControlTempi ate.Triggers> </ControlTemplate> </Page.Resources> <GroupBox Header="Options" FontSize="12pt" Horizonta1 Alignment="Center" VerticaI Alignment="Center">
690 Глава 25. Шаблоны <StackPanel> <RadioButton Template="{StaticResource newradlo}" Hon zonta 1 Al i gnment="Center" Content="RadioButton 1" /> <RadioButton Template="{StaticResource newradlo}" Ногizonta1 Al ignment="Center" Content="RadioButton 2" IsChecked="True" /> <RadioButton Tempiate="{StaticResource newradlo}" Horizontal Alignment="Center" Content="RadioButton 3" /> <RadioButton Template="{StaticResource newradlo}" Horizontal Al 1gnment="Center" Content="RadioButton 4" /> </StackPanel> </GroupBox> </Page> Свойство Content объекта Contentpresenter можно связать co свойством Content элемента следующим фрагментом разметки: <ContentPresenter Content="{TempiateBindlng Contentcontrol .Content}" /> Однако в данном XAML-файле указано, что привязка происходит автомати- чески. Элемент Border шаблона определяет свойство CornerRadius равным 100; этого должно быть достаточно для имитации эллипса в большинстве вариантов содержимого переключателей. По умолчанию свойство BorderThickness элемента Border равно 0. Секция Triggers шаблона изменяет его на 1 только при выделении элемента. В результате выделение переключателя обозначается эллипсом, заклю- чающим все содержимое (вместо маленького закрашенного кружка). Следующий XAML-файл идет еще дальше по пути переопределения внешне- го вида элемента. Он использует логику элемента CheckBox, но рисует «рубиль- ник» с двумя состояниями (Off/On). ToggleSwitch.xaml <I_ _ =============================================== ToggleSwitch.xaml (c) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Page.Resources> <ControlTempi ate x:Key="switch" TargetType="{x:Type CheckBox}"> <Grid> <Gri d.RowDefi ni ti ons> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Gri d.RowDefi ni ti ons> <Border Width="96" Height="48"
Шаблоны 691 BorderBrush="Black" BorderThickness="1"> <Canvas Background="LightGray"> <TextBlock Canvas.Left="0" Canvas.Top="0" Foreground="BlackH Text="0ff" Margin="2" /> <TextBlock Canvas.Right="0" Canvas.Top="0" Foreground="Black" Text="0n" Margin="2" /> <Line Name="lineOff" StrokeThickness="8" Stroke="Black" Xl="48" Yl="40" X2="20" Y2="16" . StrokeStartLineCap="Round" StrokeEndLineCap="Round" /> <Line Name="lineOn" StrokeThickness="8" Stroke="Black" Xl="48" Yl="40" X2="76" Y2="16" StrokeStartLineCap="Round" StrokeEndLineCap="Round" Visibility="Hidden" /> </Canvas> </Border> <ContentPresenter Grid.Row='T’ Content="{TempiateBinding Content}" HorizontalAlignment="Center" /> </Grid> <ControlTempi ate.Triggers> <Trigger Property="IsChecked" Value-"True"> <Setter TargetName="lineOff" Property="Visibility" Value="Hidden" /> <Setter TargetName="lineOn" Property="Visibility" Value="Visible" /> </Trigger> </ControlTempi ate.Tri ggers> </ControlTempiate> </Page.Resources> <CheckBox Template="{StaticResource switch}" Content="Master Switch" HorizontalAlignment="Center" Vertical Alignment="Center" /> </Page> Визуальное дерево начинается с панели Grid, состоящей из двух строк. Нижняя строка предназначена для ControlPresenter, а в верхней выводится графический переключатель. Сначала рисуется прямоугольная рамка Border с серым фоном, содержащая панель Canvas. Последняя содержит два элемента TextBlock для надпи- сей Off и On, а также две жирные линии с атрибутами Name «lineOft» и «ПпеОп». Обратите внимание: у линии lineOn свойство Visibility изначально равно Hidden.
692 Глава 25. Шаблоны Секция Triggers содержит элемент Trigger для свойства IsChecked. Когда оно истинно, свойство Visibility линии lineOff меняется на Hidden, а свойство Visibility линии lineOn становится равным Visible. Внешне все выглядит так, словно линия переключается между двумя состояниями. Ранее я упоминал, что у любого элемента с визуальным оформлением, опреде- ляемого в WPF, свойству Template изначально задается объект типа ControlTem- plate. Задавая свойству Template собственный объект ControlTemplate, вы фактиче- ски заменяете исходный шаблон. Если вы захотите создать объект ControlTemplate, соответствующий стандартам исходных шаблонов, необходимо внимательно изу- чить эти шаблоны. Именно для этой цели был написан наш следующий пример. Программа DumpControlTeinplate содержит команду меню Control, которая вы- водит перечень всех открытых классов, производных от Control, в виде иерархии, имитирующей иерархию наследования. Пользователь выбирает один из элемен- тов, а программа создаст объект указанного типа и выводит его в верхней части панели Grid. Далее пользователь выбирает команду Template Property из меню Dump, а программа использует нижнюю часть Grid для вывода объекта ControlTemplate в удобном формате XAML. Код X/YML выводится в поле TextBox, что позволяет скопировать и вставить его в текстовый редактор для печати. Проект DumpControlTeinplate состоит из трех файлов с исходным кодом. Класс ControlMenuItem, производный от Menuitem, конструирует меню со всеми классами, производными от Control. В поле Header каждой команды меню выво- дится имя класса; свойство Тад содержи!’ объект Туре для указанного класса. ControlMenuItem.cs //------------------------------------------------ // ControlMenuItem.cs (с) 2006 by Charles Petzold //------------------------------------------------ using System; us1 ng System.Col 1ect1ons.Generic; using System.Reflection; using System.Windows; using System.Windows.Controls; namespace Petzold.DumpControlTempi ate { public class ControlMenuItem : Menuitem { public ControlMenuItemO { // Получение сборки, в которой определяется класс Control Assembly asbly = Assembly.GetAssembly(typeof(Control)); // Массив всех типов Type[] atype = asbly.GetTypes(): // Потомки Control будут храниться в отсортированном списке SortedLlst<stnng. Menultem> sortlst = new SortedLlst<str1ng. Menultem>(); Header = "Control";
Шаблоны 693 Tag = typeof(Control); sort1st.Add("Control", this): // Перебор всех типов в массиве. // Для класса Control и его потомков создаются команды меню, // включаемые в объект SortedLlst. // Обратите внимание: в свойстве Тад команды меню // хранится соответствующий объект Туре. foreach (Type typ In atype) { If (typ.IsPublic && (typ.IsSubclassOf(typeof(Control)))) { Menuitem Item = new MenuItemO; Item.Header = typ.Name; Item.Tag = typ: sortlst.Add(typ.Name, Item): } } // Перебор содержимого отсортированного списка // и назначение родителей команд меню foreach (KeyValuePalr<str1ng, Menultem> kvp In sortlst) { If (kvp.Key != "Control") { string strParent = ((Type)kvp.Value.Tag).BaseType.Name; Menuitem ItemParent = sortlst[strParent]; itemParent.Items.Add(kvp.Value): } // Повторный перебор: // Если тип является абстрактным, а его выделение невозможно, // он блокируется. foreach (KeyValuePa1r<str1ng, Menultem> kvp In sortlst) { Type typ = (Type)kvp.Value.Tag: If (typ.IsAbstract && kvp.Value.Items.Count == 0) kvp.Value.IsEnabled = false; If (’typ.IsAbstract && kvp.Value.Items.Count > 0) { Menuitem Item = new MenuItemO: Item.Header = kvp.Value.Header as string; Item.Tag = typ: kvp.Value.Items.Insert(0, Item):
694 Глава 25. Шаблоны Файл DumpControlTemplate.xaml содержит описание макета окна программы. Меню начинается с объекта ControlMenuItem, которому присваивается обработчик события Click для всех команд подменю. Подменю Dump состоит из двух команд: для вывода свойств Template и ItemsPanel (вторая применима только к элемен- там, производным от ItemsControl). Файл завершается панелью Grid, занимающей оставшуюся область окна программы. DumpControlTemplate.xaml <।_ _ ====================================================== DumpControlTemplate.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:src="clr-namespace:Petzold.DumpControlTempi ate" x:Class="Petzold.DumpControlTempi ate.DumpControlTempi ate" Title="Dump Control Template - no control"> <DockPanel> <Menu DockPanel,Dock="Top"> <src:ControlMenuItem MenuItem.Click="ControlItemOnClick"/> <MenuItem Header="Dump" SubmenuOpened="DumpOnOpened"> <MenuItem Header="Template property (type ControlTemplate)" Name="itemTemplate" Click="DumpTemplateOnClick" /> <MenuItem Header="ItemsPanel property (type ItemsPanelTemplate)" Name="itemItemsPanel" Click="DumpItemsPanelOnClick" /> </MenuItem> </Menu> <Gr1d Name="grid"> <Gri d.RowDefi ni tions> <RowDefinition Height="2*" /> <RowDefinition Height="Auto" /> <RowDefinition Height="8*" /> </Grid.RowDefi ni ti ons> <Gr1dSplitter Grid.Row="l" Height="6" HorizontalAlignment="Stretch" VerticalAlignment="Center" /> <TextBox Grid.Row="2" Name="txtbox" FontFamily="Lucida Console" AcceptsReturn="True" Hori zontalScrol1BarVi si bi 1i ty="Auto" Verti ca1Scrol1BarVi si bi 1ity="Auto" /> </Grid> </DockPaneT /Window>
Шаблоны 695 файл DumpControlTemplate.cs в основном содержит обработчики Click для ко- манд меню. Обработчик ControlItemOnClick создает элемент выбранного типа и по- мещает его в верхнюю ячейку Grid (мой опыт показывает, что свойство Template становится отличным от null только в том случае, если у элемента имеется роди- тель). Команды меню Dump преобразуют объект в XAML для вывода. DumpControlTemplate.cs ц----------------------------------------------------- // DumpControlTemplate.cs (с) 2006 by Charles Petzold //---------------------------------------------------- using System; using System.Reflection; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Markup; using System.Xml; namespace Petzold.DumpControlTempi ate { public partial class DumpControlTempi ate : Window { Control Ctrl; [STAThread] public static void MainO { Application app = new Application(); app.Run(new DumpControlTempiate()); } public DumpControlTempiate() { Initial!zeComponent(); } // Обработчик события для меню Control void ControlItemOnClick(object sender, RoutedEventArgs args) { // Удаление всех существующих дочерних объектов // из первой строки Grid for (int i = 0; i < grid.Children.Count; i++) if (Grid.GetRow(grid.Children[i]) == 0) { grid.Chi 1dren.Remove(grid.Children[i]); break;
696 Глава 25. Шаблоны // Очистка объекта TextBox txtbox.Text = ""; // Получение класса Control для команды меню, // на которой был сделан щелчок Menuitem item = args.Source as Menuitem: Type typ = (Type)item.Tag: // Подготовка к созданию объекта указанного типа Constructorlnto info = typ.GetConstructor(System.Type.EmptyTypes); // Попытка создания объекта указанного типа try { Ctrl = (Control)info.Invoke(nul1); } catch (Exception exc) { MessageBox.Show(exc.Message, Title): return: } // Попытка размещения объекта в панели Grid. // Если попытка завершается неудачей, // вероятно, это объект Window try { grid.Children.Add(ctrl); } catch { if (Ctrl is Window) (Ctrl as Window).Show(); else return: } Title = Title.Remove(Title.IndexOf('-')) + " + typ.Name; // Снятие блокировки при открытии меню Dump void DumpOnOpened(object sender. RoutedEventArgs args) { ItemTemplate.IsEnabled = Ctrl != null: itemltemsPanel.IsEnabled = Ctrl != null && Ctrl is ItemsControl; // Вывод объекта Template, // связанного co свойством ControlTemplate void DumpTemplateOnClick(object sender, RoutedEventArgs args)
Шаблоны 697 { If (Ctrl != null) Dump(ctrl.Template); // Вывод объекта ItemsPanelTemplate. // связанного co свойством ItemsPanel void DumpItemsPanelOnClIck(object sender. RoutedEventArgs args) { If (Ctrl != null && Ctrl Is ItemsControl) Dump((ctrl as ItemsControl).ItemsPanel); } // Вывод шаблона void Dump(FrameworkTemplate template) If (template != null) { // Вывод кода XAML в TextBox XmlWrlterSettlngs settings = new XmlWr1terSett1ngs(); settings.Indent = true; settings.IndentChars = new str1ng(' ’.4); sett1ngs.NewLlneOnAttrlbutes = true; StringBuilder strbulld = new StrlngBullderO; XmlWrlter xmlwrite = XmlWrlter.Create(strbu11d. settings); try { XamlWrlter,Save(template, xmlwrlte); txtbox. Text = strbulld. ToStrlngO; } catch (Exception exc) { txtbox.Text = exc.Message; } } else txtbox.Text = "no template"; } } Эти шаблоны предоставляют полное описание визуального оформления каж- дого элемента, определяемого в WPF, включая ссылки на классы SystemParameters и SystemColors. Вы найдете в них массу полезных идеи для проектирования соб- ственных шаблонов, а возможно, переймете некоторые приемы мастеров. Некоторые элементы используют другие классы для воспроизведения своего оформления. Так, класс Button широко использует класс ButtonChrome, определяе- мый в пространстве имен Microsoft.Windows.Themes. Класс ScrollBar тоже использует Класс ScrollChrome, определяемый там же. Элементы TextBox и RichTextBox обладают Относительно простыми шаблонами, потому что они фактически представляют
698 Глава 25. Шаблоны собой элементы Border с вложенными элементами Scrollviewer, содержащими ре- дактор, реализованный полностью на программном уровне. Итак, мы выяснили, как задать свойству Template объекта Button (или Radio- Button, или CheckBox) объект типа ControlTemplate для полного переопределения визуального оформления элемента. Вероятно, в большинстве случаев это потре- бует большего объема работы, чем вам хотелось бы. Чаще возникает другая, бо- лее простая задача — подправить вывод содержимого элемента. Элемент ContentPresenter отвечает за вывод содержимого во всех элементах, производных от Contentcontrol. ContentPresenter делит объекты на две категории: производные от UIEIement и все прочие. Для второй категории ControlPresenter ис- пользует метод ToString объекта для получения текстового представления объек- та. Благодаря этой особенности элементы, производные от Contentcontrol, спо- собны отображать практически любой тип содержимого — но возможно, вы хотите чуть большей гибкости? Скажем, задать содержимому Contentcontrol объ- ект, не производный от UIEIement, но чтобы его отображение не ограничивалось банальным вызовом ToString? Для примера возьмем следующий класс с определением пары простых свойств. Employee.cs //------------------------------------------- // Employee.cs (с) 2006 by Charles Petzold //------------------------------------------- using System; namespace Petzold.ContentTempl ateDemo { public class Employee { // Приватные поля string name; string face; DateTime birthdate; bool lefthanded; // Непараметризованный конструктор, используемый в XAML public EmployeeО { } // Конструктор с параметрами, используемый в коде C# public Emplоуее(string name, string face, DateTime birthdate, bool lefthanded) { Name = name; Face = face; BirthDate = birthdate; LeftHanded = lefthanded; } // Открытые свойства public string Name
Шаблоны 699 { set { name = value; } get { return name; } } public string Face { set { face = value; } get { return face: } public DateTime BirthDate set { birthdate = value: } get { return birthdate: } } public bool Lefthanded { set { lefthanded = value; } get { return lefthanded; } } } } Свойство Face определяется как строка, но в действительности оно содержит имя графического файла с портретом работника. Остальные свойства достаточ- но тривиальны. Допустим, существует несколько объектов типа Employee, и вы хотите ото- бражать их на кнопках. Конечно, можно просто задать объект Employee свойству Content кнопки, но в Employee даже не определен метод ToString, поэтому вы по- лучите бесполезную строку «Petzold.ContentTemplateDemo.Employee». Вы уже знаете, как вывести на Button сложный объект. Свойству Content объ- екта Button задается панель, на которой размещаются дочерние элементы для раз- личных свойство класса Employee (вероятно, TextBlock и Image). Вероятно, в коде C# было бы выбрано именно такое решение, потому что в нем можно использо- вать цикл for или foreach. Одиако в XAML возможен другой способ — определить специальный шаб- лон для отображения объектов Employee, а затем задать объект Employee свойст- ву Content объекта Button. Как вы знаете, Contentcontrol использует объект ContentPresenter для вывода со- держимого. Contentpresenter определяет свойство ContentTemplate типа DataTemplate; этому свойству задается шаблон, используемый для отображения содержимого. Свойство ContentTemplate также предоставляется самим классом Contentcontrol. По умолчанию оно равно null. Вы можете задать свойство ContentTemplate в элементе Button точно так же, как задаете свойство Template. А можно создать ресурс типа DataTemplate и задать его свойству ContentTemplate объекта Button. А еще мож- но создать новый класс (допустим, EmployeeButton) и определить ContentTemplate в этом классе, как это сделано в следующем примере.
700 Глава 25. Шаблоны EmployeeButton.xaml <I_ _ ================================================= EmployeeButton.xaml (с) 2006 by Charles Petzold <Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:src="clr-namespace:Petzold.ContentTemplateDemo" x:Class="Petzold.ContentTemplateDemo.EmployeeButton"> <Button.ContentTempl ate> <DataTemplate DataType="{x:Type src:Employее}"> <DockPanel> <Image DockPanel.Dock="Left" Stretch="None" Source="{Binding Path=Face}" /> <UniformGrid Rows="2" VerticalAlignment="Center" Margin="12"> <TextBlock FontSize="16pt" TextAlignment="Center" Text="{Binding Path=Name)"/> <StackPanel Orientation="Horizontal" TextBlock.FontSi ze="12pt"> <TextBlock Text="{Binding Path=BirthDate.Month}" /> <TextBlock Text-"/" /> <TextBlock Text="{Binding Path=BirthDate.Day}" /> <TextBlock Text="/" /> <TextBlock Text="{Binding Path=BirthDate.Year}" /> </StackPanel> </UniformGrid> </DockPanel> </DataTemplate> </Button.ContentTemplate> </Button> Атрибут x:Class в корневом элементе указывает на то, что в файле определя- ется новый класс. Определение DataTemplate заключено в элемент свойства Button. ContentTemplate. Для атрибута DataType отображаемого объекта (Employee) необ- ходим префикс пространства имен XML src. Как и объекты ControlTemplate, объект DataTemplate обычно начинается с визу- ального дерева. В данном случае дерево состоит из напели DockPanel с элемен- том Image, пристыкованным у левого края, и панели UniformGrid, занимающей внутреннюю область. UniformGrid содержит объект TextBlock для имени работни- ка, а также панель StackPanel с шестью объектами TextBlock для отформатирован- ной даты рождения. В элементах Image и TextBlock для обращения к различным свойствам класса Employee применяются привязки данных. Благодаря им информация из объек- тов Employee переносится в элементы визуального дерева кнопки именно так, как вам требуется. Элемент Image обладает следующим атрибутом: Source="{Binding Path=Face}" Здесь достаточно указать имя свойства Face, потому что обращение к свойст- ву будет производиться из объекта, заданного свойству Content кнопки. Первая
ц|аблоны 701 часть даты рождения выглядит чуть сложнее, потому что она обращается к кон- кретному свойству объекта BirthDate: <TextBlock Text="{Binding Path=B1rthDate.Month}" /> Далее приводится файл программной логики класса EmployeeButton. EmpioyeeButton.cs //---------------------------------------------- Ц EmployeeButton.cs (с) 2006 by Charles Petzold //---------------------------------------------- namespace Petzold.ContentTemplateDemo { public partial class EmployeeBuTton public EmployeeButton() InitializeComponent(); } } } Файл EmployeeButton.xarnl не содержи т атрибутов Name и событий, и я не знаю, зачем нужен этот файл, но я включил его по весьма веской причине: без него программа не работает. Итак, все готово к выводу информации о работниках на кнопках. Вместо портретов работников проект ContentTemplateDemo использует бесплатные изо- бражения, полученные с сайта www.sxc.hu. ContentTemplateDemo.xaml < I _ _ ====================================================== ContentTemplateDemo.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="htto://schemas.microsoft.com/winfx/2006/xaml" xml ns:src="clr-namespace:Petzold.ContentTemplateDemo" x:Class="Petzold.ContentTemplateDemo.ContentTemplateDemo" Title="ContentProperty Demo"> <Window.Resources> <Style TargetType="{x:Type sre:EmployeeButton}"> <Setter Property="HorizontalAlIgnment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="Margin" Value="12" /> </Style> ^/Window.Resources> <StackPanel Name="stack" Button.Click="EmployeeButtonOnClick">
702 Глава 25. Шаблоны <src:EmployeeButton> <s rc:EmployeeButton.Contents <src;Employee Name="Betty" BirthDate="8/31/1970" Face="Betty.png'7> </src:EmployeeButton.Content> </src:EmployeeButton> <src:EmployeeButton> <src:EmployeeButton.Content> <src:Employee Name=,,Edgar" BirthDate="2/2/1965" Face=“Edgar.png,7> </src:EmployeeButton.Content> </src:EmployeeButton> <src:EmployeeButton> <src:EmployeeButton.Content> <src:Employee Name="Sally" BirthDate="7/12/1980" Face="Sally ,png'7> </src:EmployeeButton.Content> </src:EmployeeButton> </StackPanel> </Window> Свойство Content объекта EmployeeButton оформлено в виде элемента свойст- ва EmployeeButton.Content. Так как свойство ContentTemplate объекта EmployeeButton отлично от null, этот шаблон используется для отображения объекта Employee. Реальные программы такого рода обычно берут информацию о работниках из баз данных, поэтому файл программной логики ContentTemplateDemo.cs демонст- рирует код, который может использоваться в подобных случаях. Кроме того, файл обеспечивает обработку событий Click кнопки. ContentTemplateDemo.cs //---------------------------------------------------- // ContentTemplateDemo.cs (с) 2006 by Charles Petzold //---------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.ContentTemplateDemo public partial class ContentTemplateDemo : Window
Шаблоны 703 { [STAThread] public static void MainO Application app = new Application); app.Run(new ContentTemplateDemoO): } public ContentTemplateDemoO { Initial IzeComponentO; // Добавление нового объекта EmployeeButton // (просто для демонстрации кода) EmployeeButton btn = new EmployeeButtonO; btn.Content = new EmployeeC'JIm", "Jim.png", new DateTime(1975, 6. 15), false): stack.Children.Add(btn); } // Обработчик события Click для кнопки void EmployeeButtonOnClIck(object sender, RoutedEventArgs args) { Button btn = args.Source as Button: Employee emp = btn.Content as Employee: MessageBox.Show(emp.Name + " button clicked!", Title); При сколько-нибудь заметном количестве работников панель StackPanel по- мещается в элемент Scrollviewer. В следующей главе я представлю другие прие- мы обращения к данным и их отображения в элементах. В файле EmployeeButton.xaml свойству DataType объекта DataTemplate задавался класс Employee. Может показаться, что это необходимо для правильного сопостав- ления различных свойств привязки, по это не так. Если убрать атрибут DataType из элемента DataTemplate, программа все равно будет работать. Конечно, атрибут DataType предоставляет полезную информацию для людей, которые будут читать XAML-файл, но в действительности он решает обратную задачу и используется для поиска ресурсов, соответствующих типу данных свойства Content. В следующем автономном XAML-файле четыре объекта DataTemplate опреде- ляются в виде ресурсов. Каждый шаблон обладает своим значением DataType (Int32, Double, String и DateTime), что позволяет легко определить, какой шаблон используется в каждом конкретном случае. AutoTemplateSelection.xaml <!_ _ ======================================================== AutoTemplateSelection.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml"
704 Глава 25. Шаблоны xmlns:s="clг-namespace:System;assembly=mscor 1 ib"> <Page.Resources> <DataTemplate DataType="{x:Type s:Int32}"> <StackPanel Or1entation="Horizontal"> <TextBlock Text="Integer: " /> <TextBlock Text="{Binding}" /> </StackPanel> </DataTemplate> <DataTemplate DataType="{x:Type s:Double}"> <StackPanel Orientation="Horizontal"> <TextBlock Text="Double: " /> <TextBlock Text="{Bind1ng}" /> </StackPanel> </DataTemplate> <DataTemplate DataType="{x:Type s:String}"> <StackPanel Orientation="Horizontal"> <TextBlock Text="String: " /> <TextBlock Text=,,{B1nding}" /> </StackPanel> </DataTemplate> <DataTemplate DataType="{x:Type s:DateTime}"> <StackPanel Orientation="Horizontal"> <TextBlock Text="DateTime: " /> <TextBlock Text="{Binding Path=Month}" /> <TextBlock Text="/" /> <TextBlock Text="{B1nding Path=Day}" /> <TextBlock Text=,7" /> <TextBlock Text="{B1nding Path=Year}" /> </StackPanel> </DataTemplate> <Style TargetType="{x:Type Button}"> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property=,,Margin" Value="12" /> <Setter Property="FontSize" Value="12pt" /> <Setter Property="Padding" Value="10" /> </Style> </Page.Resources> <StackPanel> <Button> <s:Int32> 27 </s:Int32> </Button> <Button> <s:Double>27.543</s:Double> </Button>
Шаблоны 705 <Button> 27.543 </Button> <Button> <x:Static Member="s:DateTime.Now" /> </Button> </StackPanel> </Page> Четыре объекта Button в конце файла обладают разными типами Content. Типы успешно сопоставляются с четырьмя объектами DataTemplate, хранящимися в виде ресурсов, без прямого связывания кнопок с ресурсами. Объект Contentcontrol ав- томатически ищет среди ресурсов элемент DataTemplate, соответствующий типу содержимого (по аналогии с тем, как элемент Button ищет стиль, у которого оп- ределен атрибут TargetType со значением Button). Мы рассмотрели один из вариантов создания разных вариантов представле- ния для разных типов содержимого. В основу другого, более гибкого способа за- ложено свойство ContentTemplateSelector, определяемое классом Contentcontrol. Это свойство относится к типу DataTemplateSelector — классу, содержащему виртуаль- ный метод с именем SelectTemplate. Вы определяете класс, производный от Data- TemplateSelector, и переопределяете метод SelectTemplate. В первом аргументе пе- редается объект, отображаемый Contentcontrol; метод должен вернуть обьект типа DataTemplate для отображения содержимого. Следующий класс из проекта SelectDataTemplate является производным от DataTemplateSelector и переопределяет SelectTemplate. EmployeeTemplateSelector.cs //---------------------------------------------------------- И EmployeeTemplateSelector.cs (с) 2006 by Charles Petzold // using Petzold.ContentTemplateDemo; using System; using System.Windows; using System.Windows.Controls; namespace Petzold.SeiectDataTemplate { public class EmployeeTemolateSelector : DataTemplateSelector { public override DataTemplate SelectTemplate(object item. Dependencyobject container) { Employee emp = item as Employee; FrameworkElement el = container as FrameworkElement; return (DataTemplate) el,FindResource( emp.LeftHanded ? "tempiateLeft" : "tempiateRight");
706 Глава 25. Шаблоны В контексте этого проекта первый аргумент SelectTemplate представляет собой объект типа Employee, а второй аргумент — объект типа Contentpresenter. Метод выбирает один из шаблонов с ключами «templateLeft» и «templateRight» в зави- симости от значения свойства LeftHanded объекта Employee. В следующем XAML-файле определяется секция Resources, связывающая класс EmployeeTemplateSelector с ключом «selectTemplate». Далее следуют два шаб- лона с ключами «templateRight» и «templateLeft». Шаблоны практически одина- ковы, но у одного объект Image выводится справа, а у другого — слева. SelectDataTemplate.xaml <।_ _ ===================================================== SelectDataTemplate.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:emp="clr-namespace:Petzold.ContentTemplateDemo" xml ns:src="clr-namespace:Petzold.SeiectDataTemplate" x:Class="Petzold.SeiectDataTemplate.SeiectDataTemplate" Title="Select DataTemplate"> <Window.Resources> <src:EmployeeTemplateSelector x:Key="selectTemplate" /> <DataTemplate x:Key="templateRight"> <DockPanel> <Image DockPanel.Dock="Right" Stretch="None" Source="{Binding Path=Face}" /> <UniformGrid Rows="2" Vertical Alignment="Center" Margin="12"> <TextBlock FontSize="16pt" TextAlignment="Center" Text="{Binding Path=Name}"/> <StackPanel Orientation="Horizontal" TextBlock.FontSi ze="12pt"> <TextBlock Text="{Binding Path=BirthDate.Month}" /> <TextBlock Text='7" /> <TextBlock Text="{Binding Path=BirthDate.Day}" /> <TextBlock Text="/" /> <TextBlock Text="{Binding Path=BirthDate.Year}" /> </StackPanel> </UniformGrid> </DockPanel> </DataTemplate> <DataTemplate x: Key="templatel_eft"> <DockPanel> <Image DockPanel.Dock="Left" Stretch="None" Source="{Binding Path=Face}" /> <UniformGrid Rows="2" VerticalAlignment="Center" Margin="12"> <TextBlock FontSize="16pt" TextAlignment="Center" Text="{Binding Path=Name}"/>
Шаблоны 707 <StackPanel Or1entat1on="Hor1zontal" TextBlock.FontSIze="12pt"> <TextBlock Text="{B1nd1ng Path=B1rthDate.Month}" /> <TextBlock Text='7" /> <TextBlock Text="{Binding Path=B1rthDate.Day}" /> <TextBlock Text='7" /> <TextBlock Text="{B1nd1ng Path=B1rthDate.Year}" /> </StackPanel> </Un1formGrid> </DockPanel> </DataTemplate> <Style TargetType="{x:Type Button}"> <Setter Property="Hor1zontalA11gnment" Value="Center" /> <Setter Property="Vert1calAlignment" Value="Center" /> <Setter Property="Marg1n" Value="12" /> </Style> </Window.Resources> <StackPanel> <Button ContentTemplateSelector="{StaticResource selectTemplate}"> <Button.Content> <emp:Employee Name="Betty" B1rthDate="8/31/1970" Face="Betty.png" LeftHanded="False"/> </Button.Content> </Button> <Button ContentTemplateSelector="{StaticResource SelectTemplate}"> <Button.Content> <emp:Employee Name="Edgar" B1rthDate="2/2/1965" Face="Edgar.png" LeftHanded="True,7> </Button.Content> </Button> <Button ContentTemplateSelector="{StaticResource sei ectTemp1 ate}"> <Button.Content> <emp:Employee Name="Sally" B1rthDate="7/12/1980" Face="Sally.png" LeftHanded="True,7> </Button.Content> </Button> <Button ContentTemplateSelector="{StaticResource sei ectTemp1 ate}"> <Button.Content>
708 Глава 25. Шаблоны <emp:Employee Naine=,,J1nr' BirthDate="6/15/1975" Face^"Jim.png" LeftHanded="False'7> </Button.Content> </Button> </StackPanel> </Window> Остаток XAML-файла описывает макет окна. Четыре кнопки требуют лишь задать ContentTemplateSelector ресурс «seleciTemplate» (объект EmployeeTemplate- Selector), который в дальнейшем определяет шаблон, используемый для отобра- жения содержимого. Следующий! файл завершает проект. SelectDataTemplate.cs //---------------------------------------------------- // Se iectDataTemplate.es (с) 2006 by Charles Petzold // using System; using System.Windows; namespace Petzold.SeiectDataTemplate { public partial class SeiectDataTemplate : Window [STAThread] public static void MainO Application app = new ApplicationO; app.Run(new SeiectDataTemplateO); } public SeiectDataTemplate() InitializeComponent(); Хотя проект SelectDataTcmplatc демонстрирует наиболее универсальный! способ выбора шаблонов, используемых для отображения содержимого, свойст- во ContentTemplateSelector в этом конкретном случае не является обязательным. Следующий проект TriggerDataTemplate демонстрирует альтернативный подход. Как и в предыдущем проекте, часть класса Window, написанная на С#, выглядит весьма тривиально. TriggerDataTemplate.cs //---------------------------------------------------- // TriggerData7emplate.cs (с) 2006 by Charles Petzold //---------------------------- -----------------------
Шаблоны 709 using System; using System.Windows; namespace Petzold.Tri ggerDataTemplate { public partial class TriggerDataTemplate : Window { [STAThread] public static void MainO { Application app = new ApplicationO; app. Run(new TriggerDataTempl ateO); } public TriggerDataTemplateO Initial izeComponentO; } } } Следующий XAML-файл определяет всего один элемент DataTemplate с ключом «template». Элементу Image присваивается имя img, а его свойству DockPanel.Dock задается значение Left. Кроме того, DataTemplate содержит секцию Triggers с эле- ментом Binding, который содержит ссылку на свойство LeftHanded объекта Emp- loyee. Если его значение ложно, элемент Setter задает свойству DockPanel.Dock объекта Image значение Right. TriggerDataTemplate.xaml <! - - ==========================.============================ TriggerDataTemplate.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns;x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:emp="clr-namespace;Petzold.ContentTemplateDemo" x:Class="Petzold.T ri ggerDataTemplate.T ri ggerDataTemplate" Title="Trigger DataTemplate"> <W1ndow.Resources> <DataTemplate x:Key="tempiate"> <DockPanel> <Image Name="img" DockPanel.Dock="Left" Stretch="None" Source="{Binding Path=Face}" /> <UniformGrid Rows="2" Vertical Alignment="Center" Margin="12"> <TextBlock FontSize="16pt" TextAlignment="Center" Text="{Binding Path=Name}"/> <StackPanel Orientation--="Horizontal" TextBlock.FontSize="12pt"> <TextBlock Text="{Binding Path=BirthDate.Month}" /> <TextBlock Text="/" />
710 Глава 25. Шаблоны <TextBlock Text="{B1nd1ng Path=B1rthDate.Day}" /> <TextBlock Text='7" /> <TextBlock Text="{B1nd1ng Path=B1rthDate.Year}" /> </StackPanel> </Un1formGrid> </DockPanel> <Dat.aTempl ate. Tri ggers> <DataTr1gger Binding^'{Binding Path=LeftHanded}" Value="False"> <Setter TargetName="1mg" Property="DockPanel.Dock" Value="R1ght" /> </DataTr1gger> </DataTemplate.Tr1ggers> </DataTemplate> <Style TargetType="{x:Type Button}"> <Setter Property="Hor1zontalA11gnment" Value="Center" /> <Setter Property="Vert1calA11gnment" Value="Center" /> <Setter Property="Marg1n" Value="12" /> </Style> </W1ndow.Resources> <StackPanel> <Button ContentTemplate="{StaticResource tempiate}"> <Button.Content> <emp:Employee Name="Betty" B1rthDate="8/31/1970" Face="Betty.png" LeftHanded="False"/> </Button.Contents </Button> <Button ContentTemplate="{StaticResource template}"> <Button.Content> <emp:Employee Name="Edgar" B1rthDate="2/2/1965" Face="Edgar.png" LeftHanded="True,7> </Button.Content> </Button> <Button ContentTemplate="{Stat1cResource template}"> <Button.Content> <emp:Employee Name="Sally" B1rthDate="7/12/1980" Face="Sally.png" LeftHanded="True"/> </Button.Content> </Button> <Button ContentTemplate="{StaticResource template}"> <Button.Content> <emp:Employee Name="J1m"
Шаблоны 711 BirthDate="6/15/1975" Face="Jim.png" LeftHanded="False"/> </Button.Content> </Button> </StackPanel> </Window> Помните, что свойство ContentTemplate может использоваться для определе- ния пользовательского макета любого элемента, производного от Contentcontrol, то есть значительной части всех элементов WPF. После Contentcontrol следующее значительное подмножество элементов базируется на классе ItemsControl. Он яв- ляется корневым для всех классов, предназначенных для отображения несколь- ких объектов, в том числе Menuitem, TreeViewItem, ListBox, ComboBox и ListView. Естественно, класс ItemsControl наследует свойство Template. Для ListBox свой- ство Template выглядит достаточно просто: объект Border с внутренним объектом Scrollviewer, в котором находится объект ItemsPresenter, отвечающий за отображе- ние объектов. В ItemsControl также определяется свойство ItemsPanel, определяющее самую простейшую разновидность шаблонов. Свойство относится к типу ItemsPanel- Template, а все визуальное дерево должно состоять из единственного элемента, производного от Panel. Воспользуйтесь программой DumpControlTemplate, при- веденной ранее в этой главе, — она поможет изучить свойство ItemsPanel клас- са ItemsControl и классов, производных от него. Для большинства таких классов в качестве шаблона используется StackPanel. Для меню это WrapPanel (обеспечи- вает возможность переноса команд меню верхнего уровня по строкам). Для StatusBar используется DockPanel, а для ListBox и ListView — VirtualizingStackPanel (ана- лог обычной панели StackPanel, оптимизированный для большого количества отображаемых объектов). Панель в элементе ListBox (или любом другом классе, производном от Items- Control) можно достаточно легко заменить чем-то другим. Простой пример: ListBoxWithltemsPanel.xaml < । _ _ ======================================================== Li stBoxWithltemsPanel.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <ListBox HorizontalAlignment=,,Center" VerticalAlignment="Center"> <ListBox.ItemsPanel> <1temsPanelTempi ate> <UniformGrid /> </ItemsPanelTempiate> </ListBox.ItemsPanel> <ListBoxItem>Whatever Item !</ListBoxItem> <ListBoxItem>Whatever Item 2</ListBoxItem> <ListBoxItem>Whatever Item 3</ListBoxItem>
712 Глава 25. Шаблоны <ListBoxItem>Whatever Item 4</ListBoxItem> <Li stBoxItem>Uhatever Item 5</ListBoxItem> <ListBoxItem>Whatever Item 6</ListBoxItem> <Li stBoxItem>Whatever Item 7</ListBoxItem> <ListBox!tem>Whatever Item 8</ListBoxItem> <ListBoxItem>Whatever Item 9</ListBoxitem> </ListBox> /Page> Панель VirtualizingStackPanel, обычно используемая для отображения списков ListBox, заменяется панелью UniformGrid. У обьекта ListBox по-прежнему сохраня- ется рамка, и при достаточно малом размере страницы вы увидите, что панель заключена в объект Scrollviewer. Вся логика выделения остается без изменений. Список ListBox состоит из девяти элементов, поэтому UniformGrid автоматиче- ски выбирает количество строк и столбцов равным 3. Эти значения можно пере- определить в элементе UniformGrid: <Uni formGrid ColLllпns=,,2,, /> У такого списка имеется небольшой недостаток: объек ты слишком тесно «прижимаются» друг к другу. Вероятно, проблема проще всего решается вклю- чением в элемент ListBox следующего блока XAML: <L1stBox.Resources> <Style TargetType="{x:Type ListBoxItem}"> <Setter Property=,,Padding" Value="3" /> </Sty1e> </ListBox.Pxesources> Вместо Margin также можно использовать Padding, по результат выглядит не так хорошо. Отступы Padding включаются в выделение, а отступы Margin — нет. Для отображения отдельных элементов в ItemsControl определяется свойство ItemTemplate типа DataTemplate. Ойо используется примерно так же, как свойство ContentTemplate, определяемое ContentControi, но свойство ItemTemplate управляет внешним видом всех элементов списка и содержи г ссылки на свойства данных, хранящихся в ListBox. Вспомните проект ListColorsEvcn Elegant! ier из главы 13, где на программном уровне определялся шаблон DataTemplate для отображения цветов и их названий. Следующий проект ColorLislBoxTemplate обладает такой же функциональностью, по в нем все делается на уровне ХАМ], (не считая ссыл- ки па класс NamedBrush из проекта ListNamcdBrnsbes главы 13). Проект начина- ется со следующего файла определения приложения. ColorListBoxTemplateApp.xaml <1 __ ================================ = = = = = = = = = = = = = = == = = = = = ColorL1stBoxTemplateApp.xaml (с) 2006 by Charles Petzold Application xml ns=" http: //schemas .niicrosoft.com/winfx/2006/xaml / presentation" StartupUri="ColorListBoxTempiateWindow.xaml" />
Шаблоны 713 XAML-файл окна программы содержит определение DataTemplate в секции Resources. В атрибуте DataType указан класс NamedBrush. Визуальное дерево состо- ит из горизонтальной панели StackPanel. содержащей объекты Rectangle и TextBlock. Свойство Fill объекта Rectangle привязано к свойству Brush объекта NamedBrush, а свойство Text объекта TextBlock — к свойству Text объекта NamedBrush. ColorListBoxTemplateVwindow.xaml <i - - ============================================================= ColorLIstBoxTemplateWindow.xarnl (c) 2006 by Charles Petzold <W1ndow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml" xml ns:nb="clr-namespace:Petzold.Li stNamedBrushes" Title="Color ListBox Template" Background^'{Binding ElementName=lstbox, Path=SelectedVa1ue)"> <Window.Resources> <DataTemplate x:Key="clrlstbox" DataType="NamedBrush"> <StackPanel Orientation="Horizontal"> <Rectangle Width="16" Height="16" Margin="2" Stroke="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}" Fi1l="{Binding Path=Brush}" /> <TextBlock Vertical AlignmeiiL^'Center" Text="{Binding Pathname}" /> </StackPanel> </DataTemplate> </Window.Resources> <ListBo,x Name="lstbox" ItemTemplate="{StaticResource clrlstbox}" ItemsSource="{x:Static nb:NamedBrush.All}" Width="150" Height="150" Horizonta1 Alignment="Center" Vertical Alignment="Center" SelectedValuePath="Brush" /> </Window> Окно содержит один объект ListBox, у которого свойству ItemTemplate задает- ся объект DataTemplate, определенный в виде ресурса. Таким образом, каждый элемент списка отображается в виде горизонтальной панели StackPanel с объек- тами Rectangle и TextBlock. Свойству ItemsSource объекта ListBox задается статиче- ское свойство АП объекта NamedBrush — массив всех объектов NamedBrush, сгене- рированный но данным класса Brushes пространства System.Windows.Media. При подборе материала для главы 26 мне понадобился элемент DatePicker, от- сутствовавший в первой версии WPF. Внезапно мне пришло в голову, что ка- лендарный месяц может быть представлен в виде списка ListBox, у которого свойству ItemsPanelTemplate задана панель UniformGrid с семью столбцами. Список всегда содержит 28, 29, 30 или 31 элемент в зависимости от месяца, а элементы представляют собой обычную последовательность чисел, начиная с 1. Свойство
714 Глава 25. Шаблоны FirstColumn объекта UniformGrid идеально подходит для месяцев, начинающихся не с воскресенья1. Полученный элемент DatePicker выводит перечень всех дней месяца и позво- ляет выбрать один из них. Элемент также содержит кнопки для прокрутки впе- ред и назад по месяцам. Впрочем, в остальном его возможности весьма ограни- чены — например, он практически не поддерживает клавиатурный интерфейс (вы не сможете ввести день, месяц или год с клавиатуры). Чтобы отчасти ком- пенсировать этот недостаток, я решил, что клавиши PageUp и PageDown будут повторять кнопки прокрутки, а работа клавиш и кнопок будет зависеть от состоя- ния модификаторов Shift и Ctrl. Если ни одна из клавиш Shift или Ctrl не нажата, кнопки и клавиши PageUp/PageDown прокручивают календарь вперед/назад на месяц. С нажатой клавишей Shift календарь прокручивается на год, с нажатой клавишей Ctrl — на десять лет, а с клавишами Shift+Ctrl — на столетие. Далее приводится XAML-код элемента DatePicker. Обратите внимание на ат- рибут x:Class в корневом элементе, обозначающий определение нового класса на базе UserControl. Файл начинается с секции Resources с несколькими определения- ми Style. Двум элементам RepeatButton назначается стиль, который делает кноп- ки квадратными. Два других определения Style предназначены для элементов StatusBarltem и ListBoxItem, ни один из которых явно не упоминается в XAML- файле. С другой стороны, в нем присутствуют элементы StatusBar и ListBox, по- этому стили применяются к объектам, отображаемым этими элементами. За секцией Resources начинается определение макета элемента, который пред- ставляет собой панель Grid из четырех строк. В первой строке выводятся месяц и год, по обе стороны от которых находятся элементы RepeatButton. Во второй строке выводятся дни недели. В третьей строке размещен список ListBox с днями месяца, а в четвертой — элемент CheckBox с надписью Not applicable (см. далее). DatePicker.xaml <I_ _ ============================================= DatePicker.xaml (с) 2006 by Charles Petzold <UserControl xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:g="clr-namespace:System.G1obali zation;assembly=mscorli b" x:Class="Petzold.CreateDatePicker.DatePicker"> <UserControl.Resources> <!-- Кнопки RepeatButton делаются квадратными --> <Style TargetType="{x:Type RepeatButton}"> <Setter Property="Width" Value="{Binding RelativeSource={RelativeSource self}. Path=ActualHeight}" /> <Setter Property="Focusable" Value="False" /> 1 В англоязычных странах первым днем недели считается воскресенье. — Примеч. перев.
Шаблоны 715 <Sty1е.Triggers> <DataTrigger Binding="{Binding ElementName=chkboxNul1, Path=IsChecked}" Value="True"> <Setter Property="IsEnabled" Value="False" /> </DataTrigger> </Style.Triggers> </Style> <!-- Стили для StatusBarltem (дни недели) --> <Style TargetType="{x:Type StatusBarltem}"> <Setter Property="Margin" Value=‘T‘ /> <Setter Propert<y="HorizontalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" /> </Style> <!-- Стиль для ListBoxItem (дни месяца) --> <Style TargetType="{x:Type ListBoxItem}"> <Setter Property="BorderThickness" Value=‘T‘ /> <Setter Property="BorderBrush" Value="Transparent" /> <Setter Property="Margin" Value=,T' /> <Setter Property="HorizontalContentAlignment" Value="Center" /> <Style.Triggers> <MultiTrigger> <Mu11i Tri gger.Condi ti ons> Condition Property="IsSelected" Value="True" /> Condition Property="Selector. IsSelectionActive" Value="False" /> </MultiTrigger.Conditions> <Setter Property="BorderBrush" Vaiue="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" /> </MultiTrigger> <DataTrigger Binding="{Binding ElementName=chkboxNull, Path=IsChecked}" Value="True"> <Setter Property="IsEnabled" Value="False" /> </DataTrigger> </Style.Triggers> </Style> </UserControl.Resources> <!-- Border с панелью Grid, состоящей из четырех строк --> Corder BorderThickness=,,l" BorderBrush="{DynamicResource {x:Static SystemColors.Wi ndowTextBrushKey}}"> <Grid> <Gri d.RowDefi ni t i ons> <RowDefinition Height="Auto" />
716 Глава 25. Шаблоны <RowDefjnition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </G r id.RowDefi ni ti ons> <!-- Панель Grid для элементов Label и Button --> <Grid Background^'{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" TextBlock.Foreground^' {Dynami cResource {x:Static SystemColors.Control Li ghtLightBrushKey}}"> <Gri d.ColumnDefi ni ti ons> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" /> ColumnDefinition Width="Auto" /> </Gri d.ColumnDefi ni ti ons> <RepeatButton Grid.Column="0" Content="&lt:" FontWeight="Bold" Click="ButtonBackOnClick" /> <TextBlock Name="txtblkMonthYear" Grid.Column="l" Hori zontalAlignment="Center" VerticalAlignment="Center" Margin="3" /> <RepeatButton Grid.Column="2" Content="&gt;" FontWeight="Bold" Click="ButtonForwardOnClick" /> </Grid> <!-- Элемент StatusBar c UniformGrid для дней недели --> <StatusBar Grid.Row="l" ItemsSource="{Binding Source={x:Static g:DateTimeFormatlnfo.Current Info}. Path=Abbrevi atedDayNames}"> CtatusBar.ItemsPanel> <1temsPanelTempiate> <UniformGrid Rows="l" /> </ItemsPanelTempiate> </StatusBar.ItemsPanel> </StatusBar> <!-- Элемент ListBox c UniformGrid для дней месяца --> Corder Grid.Row="2" BorderThickness="0 1 0 1" BorderBrush="{DynamicResource {x:Static SystemColors.Wi ndowTextBrushKey}}"> <ListBox Name="lstboxMonth" Seiecti onChanged="Li stBoxOnSelecti onChanged"> <ListBox.ItemsPanel> <ItemsPanelTempiate> <Uni formGrid Name="unigridMonth" Columns="7" Rows="6"
Шаблоны 717 IsItemsHost="True" Background^'{DynamicResource {x:Static SystemColors.Control LightBrushKey}}" /> </ItemsPanelTempiate> </ListBox.ItemsPanel> <Li stBoxItem>dummy 1tem</Li stBoxItem> </ListBox> </Border> <!-- Элемент CheckBox для Null-дат --> <CheckBox Name=,,chkboxNull" Grid.Row="3" Margin="3" HorizontalAlignment="Center" Verti ca1 Ali gnment="Center" Checked="CheckBoxNullOnChecked" Unchecked="CheckBoxNullOnUnchecked"> Not applicable </CheckBox> </Grid> </Border> </UserControl> В первой строке основной панели Grid размещена другая панель Grid из трех столбцов, содержащих два элемента RepeatButton и элемент TextBlock для месяца и года. Во второй строке основной панели Grid выводятся дни недели. Я решил ис- пользовать для этой части календаря элемент StatusBar — прежде всего, потому, что StatusBar отображает несколько объектов, но не допускает пх выделение. Обычно StatusBar использует для отображения объектов DockPanel, но я переоп- ределил свойство ItemsPanel и задал ему панель UniformGrid из семи столбцов. Свойству ItemsSource задается выражение Binding, ссылающееся на свойство Abb- reviatedDaysNames класса DateTimeFormatlnfo. Это позволяет выводить названия дней недели на языке пользователя. К сожалению, механизм некорректно ра- ботает для стран, в которых неделя начинается не с воскресенья. Для таких слу- чаев необходимо проверить свойство FirstDayOfWeek класса DateTimeFormatlnfo и внести ряд изменений вручную (вероятно, в код С#). Третью строку основной панели Grid занимает объект ListBox, у которого свой- ству ItemsPanel задана другая панель UniformGrid из семи столбцов. Эта панель предназначается для дней месяца и заполняется в коде С#. Для этой цели в эле- мент ListBox включается атрибут Name со значением IstboxMonth. Как упоминалось ранее, код C# также должен задать свойство FirstColumn панели UniformGrid, по- этому последней задается атрибут Name со значением unigridMonth. В четвертой и последней строке размещается элемент CheckBox. Я хотел, что- бы пользователь мог выбрать null-дату, и для данного элемента DatePicker такой флажок показался мне самым простым и прямолинейным решением. Как вы уже знаете, когда в коде XAML определяются элементы управления и интерфейсные элементы, обычно находящиеся в окне, на странице или на па- нели, строка, присваиваемая атрибуту Name какого-либо элемента, становится
718 Глава 25. Шаблоны именем переменной, которая может использоваться в кодовой части класса. Од- нако атрибуты Name в шаблонах работают иначе. Имена не становятся именами переменных, потому что один шаблон может использоваться в нескольких эле- ментах. Вместо этого для поиска элемента в шаблоне необходимо использовать метод FindName, определяемый классом FrameworkTemplate. Метод FindName воз- вращает null, если элемент в шаблоне еще не был воплощен в реально сущест- вующий объект. Процесс создания объектов по шаблонам происходит при за- грузке и прорисовке элементов. Чтобы метод FindName возвращал актуальные данные, вызовите перед FindName метод ApplyTemplate — этот метод, определяе- мый классом FrameworkElement, строит визуальное дерево. Но самые большие проблемы у элемента DatePicker возникли с панелью Uni- formGrid, предназначенной для вывода дней месяца. Эта панель обладает атрибу- том Name (unigridMonth), однако она спрятана глубоко в шаблоне ItemsPanel объекта UstBox. Опробовав несколько разных вариантов, я остановился на рекурсивном методе, использующем VisualTreeHelper для поиска UniformGrid. DatePicker.cs является файлом программной логики класса DatePicker. Класс оп- ределяет свойство Date, поддерживаемое зависимым свойством DateProperty. Оно представляет собой объект DateTime с возможностью присваивания null, где по- следнее означает, что объект DateTime неприменим в конкретном контексте. На- пример, в следующей главе элемент DatePicker используется на панелях ввода дат рождения и смерти. Если человек еще жив, дата смерти представлена значе- нием null. Впрочем, использование DateTime с возможным значением null создает своп проблемы. К свойствам такого объекта нельзя обращаться напрямую — необхо- димо сначала преобразовать его в обычный объект DateTime. Для значения null такое преобразование завершится неудачей. Класс DatePicker определяет событие DateChanged, поддерживаемое маршру- тизируемым событием DateChangedEvent. Как следует из названия, событие про- исходит при изменении свойства Date. DatePicker.cs //------------------------------------------- // DatePicker.cs (с) 2006 by Charles Petzold //------------------------------------------- using System; using System.Globalization: using System.Windows: using System.Windows.Controls; usi ng System.Windows.Controls.Primitives: using System.Windows.Input; using System.Windows.Media; namespace Petzold.CreateDatePicker { public partial class DatePicker {
Шаблоны 719 UniformGrid unigridMonth; DateTime datetimeSaved = DateTime.Now.Date; // Определение зависимого свойства DateProperty public static readonly Dependencyproperty DateProperty = DependencyProperty.RegisterC'Date", typeof(DateTime?), typeof(DatePicker), new PropertyMetadata(new DateTimeO, DateChangedCallback)); // Определение маршрутизируемого события DateChangedEvent public static readonly RoutedEvent DateChangedEvent = EventManager.RegisterRoutedEvent("DateChanged", Routi ngStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler<DateTi me?>), typeof(DatePicker)); // Конструктор public DatePickerO { Initial!zeComponent(); // Инициализация свойства Date Date = datetimeSaved; // Установка обработчика для события Loaded Loaded += DatePickerOnLoaded; // Обработчик для события Loaded окна void DatePickerOnLoaded(object sender, RoutedEventArgs args) unigridMonth = FindUniGrid(lstboxMonth); if (Date != null) { DateTime dt = (DateTime)Date; unigridMonth.FirstColumn = (int)(new DateTime(dt.Year, dt.Month, 1).DayOfWeek); } } // Рекурсивный метод для поиска UniformGrid UniformGrid FindUniGrid(DependencyObject vis) { if (vis is UniformGrid) return vis as UniformGrid; for (int i = 0; i < VisualTreeHelper.GetChiIdrenCount(vis); i++) Visual visReturn = FindUniGrid(VisualTreeHelper.GetChild(vis, i)); if (visReturn != null)
720 Глава 25. Шаблоны return visReturn as UniformGrid; return null; // Свойство Date поддерживается зависимым свойством DateProperty public DateTime? Date set { SetValue(DateProperty, value); } get { return (DateTime?)GetValue(DateProperty); } // Событие DateChanged поддерживается // маршрутизируемым событием DateChangedEvent public event RoutedPropertyChangedEventHandler<DateTime?> DateChanged add { AddHandler(DateChangedEvent. value); } remove { RemoveHandler(DateChangedEvent. value): } // Кнопки Back и Forward void ButtonBackOnClick(object sender, RoutedEventArgs args) { FlipPage(true); void ButtonForwardOnClick(object sender. RoutedEventArgs args) { FlipPage(false); // Кнопки дублируются клавишами PageDown и PageUp protected override void OnPreviewKeyDown(KeyEventArgs args) base.OnKeyDown(args): if (args.Key == Key.PageDown) { FlipPage(true): args.Handled = true: else if (args.Key == Key.PageUp) FlipPage(false); args.Handled = false: // Переход к следующему месяцу, году, десятилетию и т.д. void FlipPage(bool isBack) {
Шаблоны 721 if (Date == null) return; DateTime dt = (DateTime)Date; int numPages = isBack ? -1 : 1; // Если нажата клавиша Shift, прокручивать список по годам if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboa rd.IsKeyDown(Key.RightShi ft)) numPages *= 12; // Если нажата клавиша Ctrl, прокручивать список по десятилетиям if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.Ri ghtCtrl)) numPages = Math.Max(-1200. Math.Min(1200, 120 * numPages)); // Вычислить новый объект DateTime int year = dt.Year + numPages / 12; int month = dt.Month + numPages % 12; while (month < 1) month += 12: year -= 1; } while (month > 12) month -= 12; year += 1; // Задание свойства Date (генерирует DateChangedCallback) if (year < DateTime.MinValue.Year) Date = DateTime.MinValue.Date; else if (year > DateTime.MaxValue.Year) Date = DateTime.MaxValue.Date; else Date = new DateTime(year, month, Math.Min(dt.Day, DateTime.DaysInMonth(year, month))); } // Если флажок CheckBox установлен, сохранить свойство Date // и задать ему null void CheckBoxNul1OnChecked(object sender, RoutedEventArgs args) { if (Date != null) datetimeSaved = (DateTime)Date; Date = mil 1; // Если флажок CheckBox снят, восстановить свойство Date void CheckBoxNul10nUnchecked(object sender, RoutedEventArgs args)
722 Глава 25. Шаблоны { Date = datetimeSaved; // При изменении выделения в ListBox изменить день месяца void ListBoxOnSelectionChanged(object sender, SeiectionChangedEventArgs args) { if (Date == null) return; DateTime dt = (DateTime)Date: // Задание нового свойства Date (генерирует DateChangedCallback) if (IstboxMonth.Selectedlndex != -1) Date = new DateTime(dt.Year, dt.Month, Int32.Parse(IstboxMonth.Selectedltem as string)); // Метод инициируется свойством DateProperty static void DateChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args) { // Вызов OnDateChanged для изменяемого объекта (obj as DatePicker),0nDateChanged((DateTime?)args.01dValue. (DateT i me?)a rgs.NewVa1ue); } // OnDateChanged изменяет визуальное оформление // в соответствии с новым значением Date protected virtual void OnDateChanged(DateTime? dtOldValue, DateTime? dtNewValue) { chkboxNull.IsChecked = dtNewValue == null; if (dtNewValue != null) { DateTime dtNew = (DateTime)dtNewValue; // Задание текста месяца и года txtblkMonthYear.Text = dtNew.ToString( DateTimeFormatInfo.Current Info.YearMonthPattern); // Задание первого дня месяца if (unigridMonth != null) unigridMonth.FirstColumn = (int)(new DateTime(dtNew.Year, dtNew.Month, 1).DayOfWeek); int IDaysInMonth = DateTime.Days InMonth(dtNew.Year, dtNew.Month); // Если количество дней не подходит, заполнить список заново if (IDaysInMonth != IstboxMonth.Items.Count)
Шаблоны 723 1stboxMonth.Begi nlnlt(); 1stboxMonth.Items.Cl ear(); for (Int 1 = 0; 1 < iDaysInMonth; i++) 1 stboxMonth. Items. Add((i + 1). ToStrlngO): 1 stboxMonth. EndlnitO; 1stboxMonth.Selectedlndex = dtNew.Day - 1: } // Теперь инициируется событие DateChangedEvent RoutedPropertyChangedEventArgs<DateTime?> args = new RoutedPropertyChangedEventArgs<DateTime?>(dt01dValue, dtNewValue, DatePicker.DateChangedEvent); args.Source = this; RaiseEvent(args): } } За конструктором, определениями свойства Date и событиями DateChanged в файле DatePicker.cs следуют методы обработки пользовательского ввода. Щелч- ки на элементах RepeatButton и нажатия клавиш Pagellp/PageDown прокручивают календарь на месяц. Основная логика находится в методе FlipPage и завершается за- данием нового значения свойства Date. Обработчик события SelectionChanged объ- екта ListBox тоже изменяет свойство Date. В определении зависимого свойства DateProperty в начале файла указано, что статический метод DateChangedCallback вызывается при каждом изменении свой- ства Date. Он вызывает метод OnDateChanged, обновляющий визуальное оформ- ление новой датой и инициирующий маршрутизируемое событие DateChanged. Для проверки кода я сконструировал небольшое окно. Один объект TextBlock привязан к свойству Date элемента DatePicker. Другой получает значение из об- работчика, установленного для события DateChanged. CreateDatePickerWindow.xaml <i __ ========================================================= CreateDatePickerWindow.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:src="clr-namespace:Petzold.CreateDatePi cker" x:Class-"Petzold.CreateDatePi cker.CreateDatePi ckerWi ndow" Title="Create DatePicker" Si zeToContent="Wi dthAndHei ght" Resi zeMode="CanMini mize"> <StackPanel> <src:DatePicker x:Name="datepick"
724 Глава 25. Шаблоны Horizontal Al ignment="Center" Margin="12" DateChanged="DatePickerOnDateChanged" /> <StackPanel Orientation="Horizontal" Margin="12"> <TextBlock Text="Bound value: " /> <TextBlock Text="{Binding ElementName=datepick, Path=Date}" /> </StackPanel> <StackPanel Orientation="Horizontal" Margin="12"> <TextBlock Text="Event handler value: " /> <TextBlock Name="txtblkDate" /> </StackPanel> </StackPanel> </Window> Обработчик события определяется в фа Гию программной логики. Он обнов- ляет TextBlock либо новой датой, либо пустой строкой, обозначающей null. CreateDatePickerWindow.cs //-------------------------------------------------------- // CreateDatePickerWindow.cs (с) 2006 by Charles Petzold //-------------------------------------------------------- using System: using System.Windows: using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media: namespace Petzold.CreateDa LePicker public partial class CreateDatePickerWindcw : Window { public CreateDatePickerWindowt) InitializeComponent(): // Обработчик события DateChanged элемента DatePicker void DatePickerOnDateChangedtobject sender. RoutedPropertyChangedEventArgs<DateTime?> args) { if (args.NewValue != null) { DateTime dt = (DateTiine)args.NewValue: txtblkDate.Text - dt.ToStringCci"): else txtblkDate.Text = ; } 1 } Проект завершается файлом определения приложения.
Шаблоны 725 CreateDatePickerApp.xaml <! - - ================================================ CreateDatePickerApp.xaml (с) 2006 by Charles Petzold Application xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Class="Petzold.CreateDatePi cker.CreateDatePi ckerApp" StartupUri="CreateDatePickerWindow.xaml" /> Хотя элемент DatePicker убедительно доказывает, что элементу ListBox можно найти нестандартное применение, чаще всего ListBox используется для вывода данных, а в наше время данные часто принимают форму XML-файлов. В сле- дующей главе я покажу, как создавать и просматривать такие файлы. А пока предположим, что данные шести работников вашей организации хра- нятся в нижеприведенном XML-файле. В следующем упражнении мы создадим объект ListBox для вывода информации об этих шести работниках. Employees.xml < । - - =========================================== Employees.xml (с) 2006 by Charles Petzold Employees xmlns=""> <Employee Name="Betty"> <Bi rthDate>8/3111970</BirthDate> <Face>Betty.png</Face> <LeftHanded>False</LeftHanded> </Employee> <Employee Name=,,Edgar"> <Bi rthDate>2/2/1965</BirthDate> <Face>Edgar.png</Face> <LeftHanded>T rue</LeftHanded> </Employee> <Employee Name="Sally,,> <BirthDate>7/12/1980</BirthDate> <Face>Sally.png</Face> <LeftHanded>True</LeftHanded> </Employee> <Employee Name="Jim"> <Bi rthDate>6/15/1975</BirthDate> <Face>Jim.png</Face> <LeftHanded>False</LeftHanded> </Employee> <Employee Name="Anne,,> <Bi rthDate>4/7/1975</Bi rthDate> <Face>Anne.png</Face> <LeftHanded>True</LeftHanded>
726 Глава 25. Шаблоны </Emplоуее> <Employee Name="John"> <В1 rthDate>12/2/1955</В1rthDate> <Face>John.png</Face> <LeftHanded>Fal se</LeftHanded> </Employee> </Employees> Данный файл является частью проекта EmployeeWheel, а его свойству Build Action задано значение Resource. Как нетрудно заметить, каждый тег Employee в XML-файле содержит ту же информацию, что и класс Employee, приводившийся ранее в этой главе. Впрочем, класс Employee не обязателен для проекта Employee- Wheel. Список ListBox и его шаблоны получают всю необходимую информацию из файла Employees.xml. Объект ListBox в проекте EmployeeWheel выводит данные шести работников по кругу; при этом используется класс RadialPanel, созданный в главе 12. Для про- екта EmployeeWheel необходимы файлы RadialButton.cs и RadialButtonOrientation.cs из проекта CircleTheButtons этой главы. Чтобы обратиться к XML-файлу из секции Resources XAML-файла, следует определить объект типа XmlDataProvider с атрибутом Source, содержащим URI файла: <XmlDataProvider x:Key="emps" Source="Employees.xml" XPath=,,Employees" /> Свойство XPath содержит строку на языке XML Path Language (см. www.w3.org/ TR/xpath), в которой не могут присутствовать функции. Также можно встроить данные XML непосредственно в XAML-файл, заклю- чив их в элемент x:XData: <XmlDataProvider x:Key="emps" XPath="Employees'^ <x:XData> Employees xmlns=""> </Employees> </x:XData> </XmlDataProvider> В обоих случаях свойству ItemsSource объекта ListBox можно задать выраже- ние Binding, содержащее ссылку на ресурс: ItemsSource="{Binding Source={Stat1cResource emps}. XPath=Employee}" Свойство XPath показывает, что каждый элемент списка представлен узлом Employee. Далее приводится полный файл EmployeeWheelWindow.xaml, в котором используется этот способ. EmployeeWheelWindow.xaml <I._ ====================================================== EmployeeWheelWindow.xaml (с) 2006 by Charles Petzold
Шаблоны 727 <Window xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:rp="cl r-namespace:Petzold.Ci rcleTheButtons" x:Class="Petzold.EmployeeWheel.EmployeeWheel" Title="Employee Wheel"> <Window.Resources> <XmlDataProvider x:Key="emps" Source="Employees.xml" XPath="Employees" /> </Window.Resources> <Grid> <ListBox Name="Istbox" HorizontalAlignment="Center" Vertical Alignment="Center" ItemsSource="{Binding Source={StaticResource emps}. XPath=Employee}" SelectedValuePath="Face"> <!-- Панель для вывода элементов списка --> <ListBox.ItemsPanel> <ItemsPanelTempiate> <rp:RadialPanel Orientation="ByHeight" /> </ItemsPanelTempiate> </ListBox.ItemsPanel> <!-- Шаблон для вывода элементов списка --> <Li stBox.ItemTemplate> <DataTemplate> <DockPanel Margin="3"> <Image DockPanel.Dock="Right" Stretch="None" Source="{Binding XPath=Face}" /> <UniformGrid Rows="3" Vertical Alignment="Center" Margin="12"> <TextBlock FontSize="16pt" TextAlignment="Center" Text="{Binding XPath=@Name}"/> <TextBlock FontSize="12pt" TextAlignment="Center" Text="{Binding XPath=BirthDate}" /> <TextBlock Name="txtblkHanded" FontSize="12pt" TextAlignment="Center" Text="Right-Handed" /> </UniformGrid> </DockPanel> <!-- Элемент DataTrlgger для LeftHanded --> <DataTemplate.T ri ggers> <DataTrigger Binding="{B1nding XPath=LeftHanded}" Value="True"> <Setter TargetName="txtblkHanded" Property="Text" Value="Left-Handed" />
728 Глава 25. Шаблоны </DataTr1gger> </DataTemplate.Triggers> </DataTemplate> </L1st.Box. ItemTemplate> </L1stBox> <!-- Объект Image в центре содержит выделенный элемент списка ListBox --> <Image Horizontal Al1gnment="Center" Vertical Al1gnnient="Center" Stretch="None" Source="{B1nd1ng ElementName=lstbox, Path=SelectedValue}" /> </Gr1d> </W1ndow> Ресурсная секция содержит объект XmlDataProvider, ссылающийся на файл Employees.xml со атрибутом XPath=”Employees”. Свойство ItemsSource объекта ListBox содержит привязку к ресурсу с XPath="Employees". После начального тега ListBox свойство ItemsPanel элемента ListBox определяет панель, используемую для ото- бражения элементов. В данном примере это RadialPanel. Свойство ItemTemplate объекта ListBox определяет способ отображения всех уз- лов Employee. И свойство Source объекта Image, и свойства Text объектов TextBlock содержат привязки, определения которых содержат только свойства XPath. У под- узлов узла Employee свойство XPath просто содержит ссылку на имя узла. Вот как выглядит привязка для элемента Image: Source="{B1nd1ng XPath=Face}" Файл Employees.xml содержит иодузлы свойств BirthDate, Face и LeftHanded, од- нако Name является атрибутом элемента Employee. Для обращения к атрибутам данных XML необходимо ставить перед именем атрибута символ Text="{B1nd1ng XPath=@Name}" Я не стал выводить данные в разных форматах в зависимости от значения LeftHanded. Вместо этого в каждом элементе списка выводится текст «Right- Handed» или «Left-Handed». По умолчанию в объекте TextBlock выводится текст «Right-Handed»: <TextBlock Name="txtblkHanded" FontS1ze="12pt" TextAl 1gnment=,,Center" Text="R1ght-Handed” /> Элемент DataTemplate тоже содержит секцию Triggers. Если элемент LeftHanded равен true, свойству Text объекта TextBlock задается строка «Left-Handed»: <DataTr1gger B1nd1ng="{B1nd1ng XPaLh=LeftHanded}” Value="True"> <Setter TargetName="txtblkHanded" Property=,,Text" Value="Left-Handed" /> </DataTr1gger> Проект Employee Wheel завершается файлом определения приложения Emplo- yeeWheelApp.xaml.
Шаблоны 729 EmployeeWheelApp.xaml <! - - ===========-=-=============================== EmployeeWheelАрр.xaml (с) 2006 by Charles Petzold <Appl i cation xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentati on" xmlns: x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Petzold.EmployeeWheel.EmployeeWheelApp" StartupUri="EmployeeWheelWindow.xaml" /> HierarchicalDataTemplate — единственный класс, производный от DataTemplate. Как подсказывает его имя, этот класс используется в сочетании с Menu или (бо- лее распространенный вариант ) TreeView для отображения иерархических дан- ных. Ранее в файле AutoTemplateSelection.xaml было показано, что элемент может использовать нужный DataTemplate, приводя тип отображаемых данных в соот- ветствие со свойством DataType ресурса DataTemplate. Данная возможность явля- ется ключом к использованию нескольких определений HierarchicalDataTemplate для отображения разнотипных элементов данных. Следующий автономный XAML-файл содержит фрагмент данных XML, со- держащий информацию об авторах и книгах. В этом XML-файле элементы Author обладают атрибутами Name и дочерними элементами BirthDate, DeathDate и Books. Элемент Books содержит несколько дочерних элементов Book. Каждый элемент Book обладает атрибутами Title и PubDate. Мы хотим вывести эти данные в эле- менте TreeView. HierarchicalTemplates.xaml <i __ ==========S=S=JS==============S========:==;========S=======S=====S HierarchicalTemplates.xaml (с) 2006 by Charles Petzold <Page xmlns-"http://schemas.microsoft com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Authors and Their Books"> <Page.Resources> <!-- Данные XML --> <XmlDataProvider x:Key="data" XPath="Authors"> <x:XData^ <Authors xm!ns=,,"> <Author Name="Jane Austen"> <Bi rthDate>l775</BirthDate> <DeathDate>1817</DeathDate> <Books> <Book Title="Sense and Sensibility"> <PubDate>1811</PubDate> </Book> <Book Title="Pride and Prejudice'^ <PubDate>1813</PubDate> </Book>
730 Глава 25. Шаблоны </Books> </Author> <Author Name="George Eliot"> <B1rthDate>1819</BlrthDate> <DeathDate>1880</DeathDate> <Books> <Book T1tle="Adam Bede"> <PubDate>1859</PubDate> </Book> </Books> <Books> <Book T1tle=,,M1ddlemarch"> <PubDate>1872</PubDate> </Book> </Books> </Author> <Author Name="Anthony Trollope"> <B1rthDate>1815<@060>/BirthDate> <DeathDate>1882</DeathDate> <Books> <Book Title="Barchester Towers"> <PubDate>1857</PubDate> </Book> <Book T1tle="The Way We Live Now"> <PubDate>1875</PubDate> </Book> </Books> </Author> </Authors> </x:XData> </XmlDataProv1der> < !-- Шаблон для элементов Author --> <Hierarchi calDataTemplate DataType="Author" ItemsSource="{Bi ndi ng XPath=Books/Book}"> <StackPanel Or1entat1on="Hor1zontal" TextBlock.FontSize="12pt"> <TextBlock Text="{B1nd1ng XPath=@Name}" /> <TextBlock Text=" (" /> <TextBlock Text="{B1nd1ng XPath=B1rthDate}" /> <TextBlock Text="-" /> <TextBlock Text="{B1nd1ng XPath=DeathDate}" /> <TextBlock Text=")" /> </StackPanel> < /H1 erarchica1DataTempl ate> < !-- Шаблон для элементов Book --> <H1erarch1calDataTemplate DataType="Book"> <StackPanel Or1entat1on="Hor1zontal" TextBlock.FontSize="lOpt">
Шаблоны 731 <TextBlock Text="{Binding XPath=@T1tle}" /> <TextBlock Text=" (" /> <TextBlock Text="{B1nding XPath=PubDate}" /> <TextBlock Text=")" /> </StackPanel> </Hi erarchi calDataTemplate> </Page.Resources> <!-- Элемент TreeView --> <TreeView ItemsSource="{B1nd1ng Source={StaticResource data}, XPath=Author}" /> </Page> Начнем с нижней части элемента TreeView. Свойство ItemsSource содержит привязку к ресурсу, обладающему ключом «data». Это объект XmlDataProvider, оп- ределяемый в начале ресурсной секции. Элемент XmlDataProvider содержит свой- ство XPath="Authors", а в привязке ItemsSource элемента TreeView указано свойст- во XPath=Author. Возможен и другой вариант: определить одно из свойств XPath в виде Authors/ Author и не указывать другое. Какой бы способ вы ни выбрали, в качестве эле- ментов верхнего уровня TreeView будут использоваться три элемента Author из данных XML. Как будут отображаться эти элементы Author? Элемент TreeView ищет среди ресурсов DataTemplate или HierarchicalDataTemplate с атрибутом DataType, равным Author, и находит его — первый элемент HierarchicalDataTemplate, определяемый в ресурсной секции файла. Шаблон содержит визуальное дерево, которое опи- сывает способ отображения элементов. В шести элементах TextBlock с размером шрифта 12 пунктов выводится атрибут Name элемента Author, элементы BirthDate и DeathDate, заключенные в круглые скобки и разделенные дефисом. Помните, что эти элементы и атрибуты задаются с использованием Binding, поэтому шаблон может включать ссылку на конвертор данных (если он необходим для правиль- ного форматирования). Вероятно, для форматирования дат стоило воспользо- ваться конвертором, но я хотел ограничить пример одним XAML-файлом. Первый шаблон HierarchicalDataTemplate не только описывает способ отображе- ния элемента Author, но и свойство ItemsSource обозначает путь XPath элементов, отображаемых для каждого автора. В данном примере он имеет вид Books/Book. Следовательно, для каждого автора в TreeView будут выводиться данные о кни- гах данного автора. Как именно они должны выводиться? Второй шаблон HierarchicalDataTemplate обладает атрибутом DataType со значением Book, он-то нам и нужен. На этот раз визуальное дерево содержит четыре элемента TextBlock для отображения атрибу- та Title и элемента PubDate. Второй шаблон HierarchicalDataTemplate не содержит свойства ItemsSource, поэтому иерархия здесь завершается, но она могла бы про- должаться намного глубже. Хотя иерархические данные удобно определять в XML, однако с таким же успехом можно воспользоваться объектами. Класс Author будет обладать свой- ствами Name, BirthDate и DeathDate, а также свойством Books — вероятно, типа
732 Глава 25. Шабл0Нк| List<Book>. Привязки почти не изменяются, не считая того, что вместо XPath ис- пользуется Path, a ItemsSource всегда ссылается на коллекцию. Многие реальные приложения используют иерархию как минимум в одной области программы — справочной системе. Секция Contents окна Help очень часто представляет собой иерархию тематических разделов. В справочном окне про- граммы YellowPad из главы 22 для вывода справочной информации используется механизм навигации. Давайте слегка обобщим механизм вывода справки, чтобы вы могли использовать его в своих собственных приложениях или усовершенст- вовать по мере необходимости. В любом проекте, в котором потребуется использовать простую справочную систему, воспользуйтесь Visual Studio для создания каталога Help в проекте. Ка- талог должен включать файл с именем HelpContents.xml, у которого свойство Build Action равно Resource. Пример файла HelpContents.xml приводится далее. Этот и все остальные файлы, приводимые до конца главы, находятся в про- екте FlowDocumentHelp. HelpContents.xml <।_ _ ============================================== HelpContents.xml (с) 2006 by Charles Petzold <HelpContents xmlns=""> <Topic Header="Copyright Information" Source="Help/Copyright.xaml" /> <Topic Header="Program Overview" Source="Help/Overview.xamr /> <Topic Header="The Menu"> <Topic Header="The File Menu" Source="Help/Fi 1 eMenu.xaml" /> <Topic Header="The Help Menu" Source="Help/HelpMenu.xaml" /> </Topic> </HelpContents> Файл состоит из вложенных элементов Topic с обязательным атрибутом Header и необязательным атрибутом Source. Как правило, элемент Source отсутствует только в элементах Topic, содержащих дочерние разделы. Каждый элемент Source определяет XAML-файл, также находящийся в каталоге Help. Overview.xaml <i__ =========================================== Overview.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Title="Program Overview"> <F1owDocumentReader> <FlowDocument> Paragraph Style="{StaticResource Header}"> Program Overview </Paragraph>
Шаблоны 733 <Paragraph> This file presents an overview of the program. </Paraqraph> <Paragraph> The description is probably several paragraphs in length. Perhaps it makes reference to the <Hyper1i nk Navi да I eUri="Fi1eMeni i.xaml">Fi1e Menu</Hyper1ink> and <Hyperlink NavigateUri="HelpMenu.xaml">Help Menu</Hyperlink>. </Paragraph> </FlowDocument> </FlowDocumentReader> </Page> Каждый раздел справки представлен файлом с элементом Раде, содержащим элементы FlowDocumentReader и FlowDocument. Первый элемент Paragraph ссыла- ется на элемент Style с тегом «Header». В каталоге Help также находится файл HelpStyles.xaml, который вы можете сделать сколь угодно простым или сложным по своему усмотрению. HelpStyles.xaml < I _ - === = = === ===х== = = = = = = = ==== = = = = = = = г=^ = =: - ---- - -------- HelpStyles.xaml (с) 2006 by Charles Petzold ResourceDictionary xml ns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Style TargetType-"{x:Type FlowDocumentReader}"> <Setter Property="ViewingMode" Value="Scrol1" /> </Style> <Style x:Key-"Center" TargetType="{x:7ype Paragraph}"> <Setter Property="TextAIignment" Value="Center" /> < /Sty1e> <Style x:Key="ProgramiName" TargetType="{x:Type Paragraph}" BasedOn-"{StaticResource Center}"> <Setter Property-'TontSize" Value="32" /> <Setter Property="FoniStyle" Value="ltalic" /> <Setter Property-'!ineHeight" Value-"24" /> < /Sty1e> <Style x:Key="Header" TargetType-"{x:Type Paragraph}”> <Setter Property-'TontSize" Value="20" /> <Setter Property-'ToiitWeight" Value-"Bold" /> <Setter Property-'!!neHeight" Value="16" /> < /Sty1e> </ResourceDicti onary>
734 Глава 25. Шаблоны Ссылка на этот файл включается в секцию Resources файла определения при- ложения, как показано в следующем файле для приложения с именем Generic GenericApp.xaml <।_ _ ============================================= GenericApp.xaml (с) 2006 by Charles Petzold <Appl1cation xml ns="http://schemas.mlcrosoft.com/wlnfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/wlnfx/2006/xaml" x:Class="Petzold.Generlc.GenerlcApp" Sta rtupUrl="Gener1cWIndow.xaml"> <App11 cation.Resources> <ResourceD1ct1onary> <ResourceD1ct1onary.MergedDIctlonarles> <ResourceD1ct1onary Source="Help/HelpStyles.xaml" /> </ResourceD1 ct1ona ry.MergedDIctlonarles> </ResourceD1ct1onary> </Appl1cat1on.Resources> < /Appl1catlon> Перед включением файла HelpStyles.xaml как ресурса файл определения при- ложения также запускает файл GenericWindow.xaml, который определяет макет простого окна, включающего меню с командой Help. GenericWindow.xaml < i__ ================================================ GenericWindow.xaml (с) 2006 by Charles Petzold < W1ndow xmlns="http://schemas.mlcrosoft.com/wlnfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/wlnfx/2006/xaml" x:Class="Petzold.Generlc.GenerlcWIndow" T1tle="Gener1c"> <DockPanel> <Menu DockPanel.Dock="Top"> <MenuItem Header="_Help"> <MenuItem Header="_Help for Generic..." Command="Help" /> </MenuItem> </Menu> <TextBox AcceptsReturn="True" /> </DockPanel> <W1ndow.CommandBInd 1 ngs> <CommandB1nd1ng Command="Help" Executed="HelpOnExecuted" /> </W1ndow.CommandBInd1ngs> </W1ndow>
Шаблоны 735 Обработчик события HelpOnExecuted для кохманды меню Help находится в фай- де программной логики окна. GenericWindow.cs // Gener1cW1ndow.cs (с) 2006 by Charles Petzold //---------------------------------------------- using Petzold.FlowDocumentHelp; using System; using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media; namespace Petzold.Generic { public partial class GenerlcWIndow : Window public Gener1cW1ndow() { InltlallzeComponentO: void HelpOnExecuted(object sender. ExecutedRoutedEventArgs args) { HelpWIndow win = new HelpWIndowO: win.Owner = this: win.Title = "Help for Generic": w1n.Show(): } } } Итак, мы добрались до класса HelpWIndow, предназначенного для вывода справочной информации. Файл HelpWindow.xaml содержит ссылки на три графи- ческих файла, также расположенных в каталоге Help. Изображения, хранящиеся в файлах, обычно используются в оглавлениях справочной системы. Я взял их из библиотеки, входящей в поставку Visual Studio, но переименовал файлы, а в од- ном случае — преобразовал изображение из значка в формат PNG. HelpWindow.xaml < । _ _ ============================================= HelpWindow.xaml (с) 2006 by Charles Petzold «Window xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml" x:Class="Petzold.FlowDocumentHelp.HelpWlndow" T1tle="Help" W1dth="600" He1ght="500"
736 Глава 25. Шаблоны W1ndowStartupLocat1on="CenterScreen" ShowInTaskbar="False"> <W1ndow.Resources> <XmlDataProv1der x:Key="data" Source="Help/HelpContents.xml" XPath="HelpContents" /> <H1erarchi calDataTempl ate DataType="Top1c" ItemsSource="{B1nd1ng XPath=Top1c}" > <!-- Каждый элемент TreeVlewItem содержит Image и TextBlock --> <StackPanel Or1entatlon="Horizontal"> <Image Name="1mg" Source="Help/HelplmageQuestlonMark.png" Marg1n="2" Stretch="None" /> <TextBlock Text="{B1nding XPath=@Header}" FontS1ze="10pt" Vertical Al1gnment="Center" /> </StackPanel> <H1era rchlca1DataTemplate.T r1ggers> <!-- Изображение закрытой книги для узлов с дочерними узлами --> <DataTr1gger B1nd1ng="{B1nd1ng Relat1veSource={Relat1veSource AncestorType={x:Type TreeVlewItem}}. Path=HasItems}" Value="True"> <Setter TargetName="1mg" Property="Image.Source" Vaiue="Help/HelplmageClosedBook.png" /> </DataTr1gger> <!-- Изображение открытой книги для развернутых узлов --> <DataTr1gger B1nd1ng="{Binding Relat1veSource={Relat1veSource AncestorType={x:Type TreeVlewItem}}, Path=IsExpanded}" Value="True"> <Setter TargetName="1mg" Property="Image.Source" Vaiue="Help/HelplmageOpenBook.png" /> </DataTr1gger> </H1erarchi calDataTemplate.Triggers> </H1 erarchi calDataTemplate> </W1ndow.Resources> <Gr1d> <Gr1d.ColumnDef1nltlons> <ColumnDef1n1t1on W1dth="33*" />
Шаблоны 737 <ColumnDef1nition W1dth="Auto" /> <Co1umnDef1n1t1on W1dth="67*" /> </Gr1d.ColumnDef1nitions> < !-- Слева находится элемент TreeView с содержимым --> <TreeV1ew Name="treevue" Gr1d.Column="0" ItemsSource="{B1nd1ng Source={Stat1cResource data}, XPath=Top1c}" SeiectedValuePath="@Source" SelectedItemChanged="TreeV1ew0nSelectedItemChanged" /> < !-- В середине находится элемент GridSplitter --> <Gr1dSpl1tter Grid.Colитп="1" W1dth="6" Hor1zontalA11gnment="Center" Vertical Al1gnment="Stretch" /> < !-- Справа находится элемент Frame --> <Frame Name="frame" Gr1d.Column="2" Navigated="Frame0nNav1gated" /> </Gr1d> </W1ndow> Почти всю первую половину файла занимает секция Resources. В оставшейся части определяется макет Window — панель Grid из трех столбцов: слева TreeView, справа Frame и в середине GridSplitter. Секция Resources начинается с элемента XmlDataProvider, ссылающегося на файл HelpContents.xml с ключом «data» и атрибутом XPath HelpContents (корневой элемент файла HelpContents.xml). Два элемента DataTrlgger выбирают графические изображения для элементов списка TreeView. Ближе к концу файла элемент TreeView привязывает свое свойство Items- Source к данным и указывает, что свойство SelectedValuePath содержит атрибут Source файла содержимого. Сначала я связал свойство Source элемента Frame со свойством SelectedValue элемента TreeView, но получилось не совсем то, что тре- бовалось, поэтому я решил связать два элемента в файле программной логики. HelpWindow.cs // И HelpWIndow.cs (с) 2006 by Charles Petzold и using System: using System.Windows: using System.Windows.Controls: using System.Windows.Data: using System.Windows.Input: using System.Windows.Media: namespace Petzold.FlowDocumentHelp {
738 Глава 25. Шаблоны public partial class HelpWIndow public HelpWIndowO { Initial1zeComponent(); treevue.Focus 0; } public HelpWindow(string strTopic): thisO { If (strTopic != null) frame.Source = new Uri(strTopic, UriKI nd.Relat1ve): // При смене выделенного элемента в списке TreeView // задать источник для элемента Frame void TreeV1ew0nSelectedItemChanged(object sender. RoutedPropertyChangedEventArgs<object> args) If (treevue.SelectedValue != null) frame.Source = new Uri(treevue.SelectedValue as string, UriKI nd.Relative): } // При переходе к новому источнику в элементе Frame // выполнить синхронизацию с TreeView void Frame0nNav1gated(object sender, NavigationEventArgs args) { If (args.Uri != null && args.Uri.OrlglnalStrlng != null && args.Uri.OrlglnalStrlng.Length > 0) FlndltemToSelect(treevue, args.Uri.OrlglnalStrlng); } } // Поиск выделяемого элемента по списку TreeView bool FlndltemToSelect(ItemsControl Ctrl, string strSource) foreach (object obj In Ctrl.Items) { System.Xml.XmlElement xml = obj as System.Xml.XmlElement; string strAttrlbute = xml.GetAttr1bute("Source”); TreeViewItem Item = (TreeViewItem) Ctrl.ItemContalnerGenerator.ContalnerFromltem(obj); // Если элемент TreeViewItem совпадает URI Frame, // выделить его If (strAttrlbute != null && strAttrlbute.Length > 0 && strSource.EndsWIth(strAttrlbute)) {
Шаблоны 739 if (Item != null && 'Item.IsSelected) Item.IsSelected = true; return true; // Развернуть узел для поиска во вложенных узлах If (Item != null) { bool IsExpanded = Item.IsExpanded; Item.IsExpanded = true; if (Item.HasItems && FindltemToSelectdtem, strSource)) return true; item.IsExpanded = IsExpanded; } } return false; } } } Второй конструктор позволяет программе, использующей класс HelpWIndow, инициализировать элемент Frame одним из документов. Обработчик события SelectedltemChanged получает выбранное значение из TreeView и задает его в Frame. Синхронизация оглавления с документом вызвала некоторые проблемы: хотя класс TreeView определяет свойства Selectedltem и SelectedValue, они доступны только для чтения. Чтобы выделить элемент в списке TreeView, необходимо ус- тановить свойство IsSelected конкретного объекта TreeViewItem. Для этого прихо- дится перебрать все элементы и найти среди них нужный. Глава получилась длинной, но тема шаблонов далеко не исчерпана. В сле- дующей главе я покажу, как группировать данные в ListBox и применять шабло- ны для вывода заголовков таких групп.
Ввод и представления данных Для нескольких поколений программистов одной из самых распространенных задач считалось создание программ ввода, редактирования и просмотра данных. В таких программах обычно создаются формы ввода данных, содержащие эле- менты, соответствующие полями записей базы данных. Допустим, вы хотите создать базу данных композиторов. Для каждого ком- позитора в базе должно храниться имя, второе имя1 и фамилия, дата рождения и дата смерти (последняя может быть равна null, если человек еще жив). Хорошей отправной точкой для создания такой базы станет определение класса, содержа- щего открытые свойства для всех полей данных. Как обычно, открытые свойст- ва предоставляют общедоступный интерфейс к приватным полям. Весьма желательно, чтобы этот класс реализовал интерфейс INotifyProperty- Changed. Строго говоря, интерфейс INotifyPropertyChanged требует только одного: чтобы класс содержал событие PropertyChanged, определенное в соответствии с де- легатом PropertyChangedEventHandler. Но такое событие имеет смысл лишь в том случае, если оно будет инициироваться при каждом изменении свойств класса. Person.cs //--------------------------------------- // Person.cs (с) 2006 by Charles Petzold // using System; using System.ComponentModel; using System.Xml.Serialization; namespace Petzold.Si ngleRecordDataEntry { public class Person: INotifyPropertyChanged { // Определение события PropertyChanged public event PropertyChangedEventHandler PropertyChanged; 1 Второе имя — у нас это обычно «отчество», но на Западе это не всегда так. — Примеч. перев.
Ввод и представления данных 741 // Приватные поля string strFIrstName = "cfirst name>": string strMiddleName = "" ; string strLastName = "clast name>"; DateTime? dtBirthDate = new DateTime(1800, 1, 1); DateTime? dtDeathDate = new DateTime(1900, 12, 31): // Открытые свойства public string FirstName set { strFirstName = value: OnPropertyChangedC"Fi rstName"): } get { return strFirstName: } } public string MiddleName { set strMiddleName = value; OnPropertyChanged("Mi ddl eName"); } get { return strMiddleName: } public string LastName { set { strLastName = value; OnPropertyChanged!"LastName"); } get { return strLastName: } } [Xml Element(DataType="date")] public DateTime? BirthDate { set { dtBirthDate = value: OnPropertyChanged("Bi rthDate"): } get { return dtBirthDate: } } [Xml Element!DataType = "date")] public DateTime? DeathDate
742 Глава 26. Ввод и представления данных set dtDeathDate = value; OnPropertyChanged("DeathDate"): } get { return dtDeathDate: } } // Инициирование события PropertyChanged protected virtual void OnPropertyChanged(string strPropertyName) { if (PropertyChanged != null) PropertyChanged(this. new PropertyChangedEventArgs(strPropertyName)); } } } Реализация интерфейса INotifyPropertyChanged позволяет использовать свой- ства класса в качестве источников привязки данных. Приемниками они быть нс могут, поскольку нс поддерживаются зависимыми свойствами, но обычно источ- ника оказывается достаточно. Метод OnPropertyChanged не является обязательным, но он хорошо подходит для инициирования события PropertyChanged и поэтому обычно включается в клас- сы, реализующие интерфейс INotifyPropertyChanged. Некоторые классы определяют метод OnPropertyChanged с аргументом PropertyChangedEventArgs; в других он опреде- ляется, как у меня, что обычно способствует получению более компактного кода. Следующий шаг — форма ввода данных. Для этой цели можно воспользоваться Window пли Раде, но самым гибким решением является какая-либо разновидность Panel. Следующий класс PersonPanel использует панель Grid для отображения трех элементов TextBox (имя, второе имя, фамилия) и двух элементов DatePicker (см. проект CreateDatePicker предыдущей главы) с датами рождения и смерти. Смысл каждого элемента ввода данных поясняется соответствующим элементом Label. PersonPanel.xaml <i __ =======================================:======= PersonPanel.xaml (с) 2006 by Charles Petzold <Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xmlns:dp="clr-namespace:Petzold.CreateDatePicker" xml ns:src="clr-namespace:Petzold.Si ngleRecordDataEntry" x:Class="Petzold.SingleRecordDataEntry.PersonPanel" > <Grid.Resources> <Style TargetType="{x:Type Label}"> <Setter Property="VerticalAlignment" Value="Center" />
Ввод и представления данных 743 <Setter Property="Margin" Value="12" /> </Style> <Style TargetType="{x:Type TextBox}"> <Setter Property="Margin" Value="12" /> </Style> <Style TargetType="{x:Type dp:DatePicker}"> <Setter Property="Margin" Vaiue="12" /> </Sty1e> </Grid.Resources> <Gri d.ColumnDef1ni tions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </G r1d.ColumnDefj n1tions> <Gr1d.RowDef1ni ti ons> <RowDefinition Height="Auto" /> <RowDefinition Height=,,Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height=,,Auto" /> </Grid.RowDefinitions> <Label Grid.Row="0" Grid.Column=,,0" Content="_First Name: " /> <TextBox Grid.Row="0" Grid.Column=,,l" Margin="12" Text="{Binding Path=FirstName. Mode=TwoWay. UpdateSourceTrigger=PropertyChanged}" /> <Label Grid.Row=,,l" Grid. Col umn^O" Content="_Middle Name: " /> <TextBox Grid.Row=,T' Grid. Columnar Margin="12" Text="{Binding Path=MiddleName. Mode=TwoWay. UpdateSourceTrigger=PropertyChanged}" /> <Label Grid.Row="2" Grid.Column="0" Content="_Last Name: " /> <TextBox Grid.Row="2" Grid.Column=,T' Margin="12" Text="{Binding Path=LastName. Mode=TwoWay. UpdateSourceTrigger=PropertyChanged}" /> <Label Grid.Row="3" Grid.Column="0" Content="_Birth Date: " /> <dp:DatePicker Grid.Row="3" Grid.Columnar Margin="12" Hori zonta1 Alignment="Center" Date="{Binding Path=BirthDate. Mode=TwoWay}" /> <Label Grid.Row="4" Grid.Column="0" Content="_Death Date: " /> <dp:DatePicker Grid.Row="4" Grid.Colитп="Г Margin=,,12" HorizontalAlignment="Center" Date--="{Binding Path=DeathDate. Mode=TwoWay}" /> </Grid>
744 Глава 26. Ввод и представления данных Три элемента TextBox содержат привязки в своих свойствах Text, а два эле- мента DatePicker содержат привязки в свойствах Date. Каждая привязка обладает свойством Path, обозначающим некоторое свойство класса Person, и для каждой привязки явно задан режим TwoWay, чтобы любое изменение в элементе отража- лось в исходных данных. Кроме того, у привязок TextBox свойству UpdateSource- Trigger задано значение PropertyChanged — исходные данные должны обновляться при каждом изменении свойства Text, а не при потере фокуса TextBox (как это происходит по умолчанию). Привязки не завершены, поскольку в них отсутствует источник данных. Как правило, источник задается в виде свойства Source или ElementName привязки, но существует и другой способ задания источника привязки — через свойство DataContext, определенное классом FrameworkElement. Главная особенность свойства DataContext заключается в том, что оно насле- дуется по визуальному дереву. Это приводит к простым, но очень важным по- следствиям: если задать свойству DataContext объекта PersonPanel объект типа Person, в элементах TextBox и DatePicker будут отображаться свойства объекта Person, а любой ввод пользователя в них будет отражен в объекте Person. Таким образом, одна привязка между объектом и свойством DataContext пане- ли превращается в несколько привязок между свойствами объекта и элемента- ми панели. Файл программной логики PersonPanel.cs выглядит тривиально. PersonPanel.cs //--------------------------------------------- // PersonPanel.cs (с) 2006 by Charles Petzold //--------------------------------------------- namespace Petzold.SingleRecordDataEntry { public partial class PersonPanel { public PersonPanel() { InitializeComponent(): } Чтобы вы лучше поняли механику привязки между объектом Person и эле- ментами PersonPanel, в первом проекте главы SingleRecordDataEntry загружаются и сохраняются файлы, содержащие только один объект Person. Любая база дан- ных, созданная этой программой, содержит единственную запись типа Person. Следующим компонентом проекта SingleRecordDataEntry станет объект Win- dow с панелью ввода PersonPanel. В окно также включено мешо с командами New, Open и Save. SingleRecordDataEntry.xaml <I_ _ ======================================================== SingleRecordDataEntry.xaml (c) 2006 by Charles Petzold
Ввод и представления данных 745 Window xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml" xml ns:pnl="clr-namespace:Petzold.SIngleRecordDataEntry" x:Class="Petzold.SIngleRecordDataEntry.SIngleRecordDataEntry" T1tle="S1ngle-Record Data Entry" S1zeToContent="W1dthAndHe1ght" ResizeMode="CanM1nimlze"> <DockPanel Name="dock"> <Menu DockPanel.Dock="Top"> <MenuItem Header="_F11e"> <MenuItem Header="_New" Command="New" /> <MenuItem Header="_Open..." Command="Open" /> <MenuItem Header="_Save..." Command^'Save" /> </MenuItem> </Menu> <!-- Панель PersonPanel для ввода информации --> <pnl .-PersonPanel x:Name="pnlPerson" /> </DockPanel> <W1ndow.CommandBindings> CommandBinding Command="New" Executed="NewOnExecuted" /> CommandBinding Command="Open" Executed="OpenOnExecuted" /> CommandBinding Command="Save" Executed="SaveOnExecuted" /<@062> </WIndow.CommandBindings> </W1ndow> Три команды меню File связываются с обработчиками Executed посредством привязок команд. Объекту PersonPanel, занимающему внутреннюю область Dock- Panel, присваивается имя pnIPerson. Три обработчика событий Executed определяются в файле программной логи- ки, завершающем проект SingleRecordDataEntry. SingleRecord DataEntry.cs //------------------------------------------------------ И S1ngleRecordDataEntry.cs (с) 2006 by Charles Petzold И------------------------------------------------------- using Microsoft.Win32; using System; using System.10; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Xml.Serialization; namespace Petzold.SIngleRecordDataEntry { public partial class SingleRecordDataEntry : Window
746 Глава 26. Ввод и представления данных const string strFilter = "Person XML files (*.PersonXml)|" + "*.PersonXml|A11 files (*.*)|*,*": XmlSerializer xml = new XmlSerializer(typeof(Person)); [STAThread] public static void MainO { Application app = new Application(): app.Run(new SingleRecordDataEntryO); } public SingleRecordDataEntryO { InitializeComponent(); // Имитация команды File>New Applicationcommands.New.Execute(null, this); // Передача фокуса первому элементу TextBox панели pnlPerson.Chi 1dren[1].Focus(); // Обработчики событий команд меню void NewOnExecuted(object sender, ExecutedRoutedEventArgs args) { pnlPerson.DataContext = new PersonO; void OpenOnExecuted(object sender, ExecutedRoutedEventArgs args) { OpenFileDialog dig = new OpenFileDialogO: dig.Filter = strFilter; Person pers; if ((bool)dlg.ShowDialog(this)) { try StreamReader reader = new StreamReader(dlg.FileName): pers = (Person) xml.Deserialize(reader); reader,Close(); catch (Exception exc) MessageBox.Show("Could not load file: " + exc.Message, Title, MessageBoxButton.OK, MessageBoxImage.Exclamati on); return: pnlPerson.DataContext = pers:
Ввод и представления данных 747 void SaveOnExecuted(object sender, ExecutedRoutedEventArgs args) f SaveFileDialog dig = new SaveFileDlalogO; dig.Filter = strFilter; if ((bool)dlg.ShowDialog(this)) { try { Streamwriter writer = new StreamWriter(dlg.FileName); xml.Serialize(writer. pnlPerson.DataContext); writer.Close(): } catch (Exception exc) MessageBox.Show("Could not save file: " + exc.Message, Title, MessageBoxButton.OK, MessageBoxImage.Exclamation); return; } } } } } Команда New (которая также имитируется в конструкторе класса) создает новый объект тина Person и задаст его свойству DataContext элемента PersonPanel. При запуске программы на панели отображаются свойства по умолчанию, опре- деленные в классе Person. Команды Open и Save вызывают диалоговые окна OpenFileDialog и SaveFileDialog соответственно. Эти диалоговые окна совместно используют строку strFilter, опре- деленную в начале класса, и по умолчанию работают с файлами, обладающими расширением PersonXML. Методы OpenOnExecuted и SaveOnExecuted также исполь- зуют один объект XmlSerializer, определяемый в начале класса для хранения объ- ектов Person в формате XML. Метод SaveOnExecuted вызывает Serialize для преоб- разования объекта Person, хранящегося в свойстве DataContext, в файл XML. Метод OpenOnExecuted загружает сохраненный ранее файл, преобразует его в объ- ект Person вызовом Deserialize, после чего задаст свойство DataContext объекта PersonPanel. При помощи программы SingleRecordDataEntry можно сохранить несколько файлов, но в каждом файле хранится только один объект Person. Типичный файл выглядит примерно так: <?xml version="l.0" encoding="utf-8"?> <Person xml ns:xsi="http://www.w3.org/2001/XMLSchema-instance" xml ns:xsd="http://www.w3.org/2001/XMLSchema"> <FirstName>Johannes</FirstName> <MiddleName />
748 Глава 26. Ввод и представления данных <LastName>Brahms</LastName> <В1rthDate>1833-05-07</BirthDate> <DeathDate>1897-04-03</DeathDate> </Person> Следующий очевидный шаг — ведение коллекции объектов Person. В .NET Framework имеется несколько классов для хранения коллекций объектов. Для простых задач я обычно использую обобщенный класс List. Однако при работе с базами данных часто бывает удобнее использовать общий класс Observable- Collection, определяемый в пространстве имен System.Collections.ObjectModel. Кол- лекция объектов Person определяется примерно так: ObservableCollection<Person> people = new ObservableCollection<Person>(); Также можно объявить класс (скажем, People) производным от класса Observ- ableCollection: public class People : 0bservableCollect1on<Person> { } Затем создается объект типа People: People people = new PeopleO: Одно из преимуществ последнего подхода заключается в том, что в класс People можно включить методы для сохранения объектов People в файлах и их последующей загрузки. (Именно этот способ используется в проекте MultiRecord- DataEntry, описанном далее.) Так как класс ObservableCollection является шаблонным, новые объекты типа Person можно добавлять в коллекцию без преобразования типа: people. Add (new PersonO): Также возможно индексирование коллекции; результат представляет собой объект типа Person: Person person = people[5]: Кроме этих двух вспомогательных возможностей, присутствующих во всех шаблонных классах коллекций, ObservableCollection также определяет два важных события: PropertyChanged и CollectionChanged. Событие PropertyChanged иницииру- ется при каждом изменении свойства одного из членов коллекции. Конечно, это происходит только в том случае, если объект ObservableCollection создан на базе класса, реализующего событие INotifyPropertyChanged и инициирующего собствен- ное событие PropertyChanged при изменении свойств (класс Person удовлетворяет этому условию). Очевидно, ObservableCollection устанавливает обработчик собы- тия PropertyChanged для каждого члена своей коллекции, а инициирует из него свое событие PropertyChanged. Событие CollectionChanged инициируется при изменении коллекции в целом, то есть при каждом добавлении, удалении, замене или перемещении элемента в коллекции.
Ввод И представления данных 749 Проект MultiRecordDataEntry включает ссылки на файлы Person.cs, Person- panel.xaml и PersonPanel.es из проекта SingleRecordDataEntry, а также файлы Date- Picker из проекта CreateDatePicker (глава 25). Файл People.cs — первый из трех новых файлов этого проекта. В нем определяется класс People, производный от ObservableCollection, предназначаемый для хранения коллекции объектов Person. people.es //--------------------------------------- // People.cs (с) 2006 by Charles Petzold //--------------------------------------- using Microsoft.Win32; using Petzold.SingleRecordDataEntry; using System; using System.10; using System.Collections.ObjectModel; using System.Windows; using System.Xml.Serialization: namespace Petzold.Muiti RecordDataEntry { public class People : ObservableCollection<Person> { const string strFilter = "People XML files (*.PeopleXml)|" + PeopleXml|A11 files (*.*)|*.*"; public static People Load(Window win) { OpenFileDialog dig = new OpenFileDialog(); dig.Filter = strFilter; People people = null; if ((bool)dlg.ShowDialog(win)) { try { StreamReader reader = new StreamReader(dlg.FileName); XmlSerializer xml = new Xml Seri alizer(typeof(People)); people = (People)xml.Deserialize(reader); reader.Close(); } catch (Exception exc) { MessageBox.ShowCCould not load file: " + exc.Message. win.Title, MessageBoxButton.OK, MessageBoxImage.Exclamati on): people = null; } } return people:
750 Глава 26. Ввод и представления данных public bool Save(Window win) { SaveFileDialog dig = new SaveFileDialogO: dig.Filter = strFilter: if ((bool)dlg.ShowDialog(win)) { try { StreamWriter writer = new StreamWriter(dlg.FileName); XmlSerializer xml = new XmlSerializer(GetTypeO): xml.Serialize(writer, this): writer.CloseO; } catch (Exception exc) { MessageBox.Show("Could not save file: " + exc.Message, win.Title, MessageBoxButton.OK, MessageBoxImage.Exclamation); return false: } } return true; } } } Я также использовал этот класс ради методов Load и Save, отображающих окна OpenFileDialog и SaveFileDialog, а затем воспользовался XmlSerializer для загрузки и сохранения объектов типа People. Обратите внимание: статический метод Load возвращает объект типа People. Оба метода, Load и Save, обладают аргументами, в которых передается родительский объект Window. Методы используют его для передачи методу ShowDialog, а также в качестве заголовка вызовов MessageBox. Show при возникновении проблем с файловым вводом и выводом. Программа MultiRecordDataEntry использует элемент PersonPanel для вывода одной записи данных, но при этом она работает с файлом, содержащим много записей. Следовательно, программа должна обладать средствами навигации по базе данных. Одно из распространенных решений — отслеживать «текущую» за- пись и создать на форме кнопки для перехода к следующей и предыдущей записи (а также к первой и последней записи). Как правило, одного лишь просмотра или редактирования записей в фай- ле недостаточно — необходимо предусмотреть возможность добавления новых и удаления существующих записей. Наряду с кнопками навигации на форме также создаются кнопки Add New и Delete. В следующем файле MultiRecordDataEntry.xaml определяется макет окна, в ко- тором присутствует меню со знакомыми командами New, Open и Save. В остав- шейся части окна размещается панель PersonPanel с шестью кнопками, которые были описаны ранее.
Ввод И представления данных 751 MultiRecordDataEntry.xaml <1 -- ================================================; MultiRecordDataEntry.xaml (c) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml" xml ns:pnl="clr-namespace:Petzold.Si ngleRecordDataEntry" x:Class="Petzol d.Mui ti RecordDataEntry.Multi RecordDataEntry" Title="Multi-Record Data Entry" SizeToContent="WidthAndHeight" Resi zeMode="CanMi nimize"> <DockPanel Name="dock"> <Menu DockPanel.Dock="Top"> <MenuItem Header="_File"> <MenuItem Header="_New" Command="New" /> <MenuItem Header="_Open..." Command="Open" /> <MenuItem Header="_Save..." Command=,,Save" /> </MenuItem> </Menu> <StackPanel> < !-- Панель PersonPanel для ввода данных <pnl:PersonPanel x:Name="pnlPerson" /> < !-- Кнопки навигации, добавления и удаления записей --> < UniformGrid Columns="6" HorizontalAlignment="Center"> <UniformGr i d.Resources> <Style TargetType="{x:Type Button}"> <Setter Property="Margin" Value="6" /> </Sty1e> </UniformGri d.Resources> <Button Name="btnFirst" Content="First" Click="FirstOnClick" /> <Button Narne=,,btnPrev" Content="Previous" Click="PrevOnClick" /> <Button Name="btnNext" Content="Next" Click="NextOnClick" /> <Button Name="btnLast" Content="Last" Click="LastOnClick" /> <Button Name="btnAdd" Content="Add New" Click="AddOnClick" /> <Button Name="btnDer Content="Delete" Click="DelOnClick" /> </UniformGrid> </StackPanel> </DockPanel> <Window.CommandBIndi ngs> CommandBInding Command="New" Executed="NewOnExecuted" /> CommandBInding Command="Open" Executed="OpenOnExecuted" /> CommandBindino Command="Save" Executed-"SaveOnExecuted" /> </W1ndow.CommandBIndi ngs> </W indow>
752 Глава 26. Ввод и представления данных Следующий файл MultiRecordDataEntry.cs завершает определение объекта Win- dow программы — в нем содержатся обработчики событий трех команд меню и шести кнопок. Класс содержит два поля: объект типа People (файл, загружен- ный в настоящий момент) и целочисленная переменная index с индексом теку- щей записи, отображаемой на панели PersonPanel. Переменная используется для индексирования объекта People и изменяется в обработчиках событий кнопок First, Previous, Next и Last. Поскольку методы Load и Save уже определены в классе People, три обработ- чика событий команд меню получаются довольно простыми. Команды New и Open создают новый объект People и вызывают метод InitializeNewPeopleObject. Метод обнуляет переменную index. Если объект People не содержит записей (для ко- манды New), метод создает запись Person и включает ее в коллекцию. В обоих случаях свойству DataContext элемента PersonPanel задается первый объект Person в коллекции People. Вызов EnableAndDisableButtons снимает блокировку с кнопки Previous, если текущая запись не является первой, и с кнопки Next, если она не является последней. MultiRecordDataEntry.cs // // Ми]tiRecordDataEntry.cs (с) 2006 by Charles Petzold // using Petzold.SIngleRecordDataEntry; using System: using System.Col 1 ections.Special 1 zed; using System.Windows: using System.Windows.Controls; using System.Windows.Input: using System.Windows.Media; namespace Petzold.MultiRecordDataEntry { public partial class MultiRecordDataEntry { People people; Int Index; [STAThread] public static void MainO { Application app = new AppHcatlonO; app.Run(new MultiRecordDataEntryО); } public MuitiRecordDataEntry() { Initial1zeComponent(); // Имитация команды F11e>New Applicationcommands.New.Execute(null. this);
рвОд и представления данных 753 // Передача фокуса первому элементу TextBox на панели pnlPerson.Chi 1dren[1].Focus(): } // Обработчики событий команд меню void NewOnExecuted(object sender, ExecutedRoutedEventArgs args) { people = new PeopleO; Initial!zeNewPeopleObject(); } void OpenOnExecuted(object sender, ExecutedRoutedEventArgs args) { people = People.Load(this); Initial!zeNewPeopleObject(); } void SaveOnExecuted(object sender, ExecutedRoutedEventArgs args) { people.Save(this); } void InitializeNewPeopleObject() { index = 0; if (people.Count == 0) people.Insert(0, new PersonO); pnlPerson.DataContext = peopleLO]: EnableAndDi sableButtons(); } // Обработчики событий для кнопок void FirstOnClick(object sender, RoutedEventArgs args) { pnlPerson.DataContext = people[index = 0]; EnableAndDi sableButtons(): } void PrevOnClick(object sender, RoutedEventArgs args) { pnlPerson.DataContext = people[index -= 1]; EnableAndDi sableButtons(); } void NextOnClick(object sender, RoutedEventArgs args) { pnlPerson.DataContext = people[index += 1]; EnableAndDisableButtons(); } void LastOnClick(object sender, RoutedEventArgs args) { pnlPerson.DataContext = people[index = people.Count - 1]; EnableAndDi sableButtons(); } void AddOnClick(object sender, RoutedEventArgs args)
754 Глава 26. Ввод и представления данных peoplе.Insert(1ndex = people.Count, new PersonO); pnlPerson.DataContext = people[index]; EnableAndDisableButtons(); } void DelOnClick(object sender. RoutedEventArgs args) { people.RemoveAt(jndex); if (people.Count == 0) people.Insert(0, new PersonO); if (index > people.Count - 1) index--; pnlPerson.DataContext = people[index]; Enabl eAndDi sableButtons(); } void EnableAndDisableButtons0 { btnPrev.IsEnabled = index к 0: btnNext.IsEnabled = index < people.Count - 1; pnlPerson.Chi 1dren[1].Focus(); } } } Обработчики событий шести кнопок весьма тривиальны. Для четырех нави- гационных кнопок обработчик просто вычисляет новое значение индекса, ис- пользует его для обращения к элементу коллекции и задает свойству DataContext панели PersonPanel полученный объект Person. Обработчик кнопки Add New создает новый объект Person и вставляет его в кол- лекцию. Кнопка Delete удаляет текущую запись. Если в результате коллекция ос- тается пустой, в нее добавляется новый объект Person. Программа MultiRecordDataEntry сохраняет и загружает файлы следую- щего вида: <?xml version="1.0" encoding="utf-8"?> <ArrayOfPerson xml ns:xsi ="http://www.w3.org/2001/XMLSchema-i nstance" xml ns:xsd="http://www.w3.org/2001/XMLSchema"> <Person> <Fi rstName>Franz</FirstName> <MiddleName /> <LastName>Schubert</LastName> <Bi rthDate>1797-01-07</BirthDate> <DeathDate>1828-11-19</DeathDate> </Person> <Person> <Fi rstName>Johannes</FirstName> <MiddleName /> <LastName>Brahms</LastName> <Bi rthDate>1833-05-07</BirthDate> <DeathDate>1897-04-03</DeathDate>
явод и представления данных 755 </Person> <Person> <Fi rstName>John</Fi rstName> <MiddleName /> <LastName>Adams</LastName> <BirthDate>1947-02-15</B1rthDate> <DeathDate xsi:nil="true" /> </Person> </ArrayOfPerson> Обратите внимание: у последнего элемента Person в этом примере свойство DeathDate равно null. В XML это значение обозначается атрибутом nil (см. специ- фикацию XML Schema по адресу http://www.w3.org/XML/Schema). Пространство имен System.Windows.Data содержит класс Collectionview, реа- лизующий логику изменения текущей записи, а также обладающий другими важными возможностями. Оставшаяся часть этой главы в основном посвящена Collectionview и его важному потомку ListCollectionView. Объект Collectionview пли ListCollectionView в действительности описывает пред- ставление коллекции, которым может быть подмножество элементов или вся коллекция, отсортированная или упорядоченная особым образом. Изменение представления не влияет на коллекцию, на базе которой это представление строилось. Объект Collectionview создается конструктором на базе коллекции: CollectionView collview = new Col 1ectionView(col 1); Аргумент coll должен реализовать интерфейс lEnumerable. Класс ListCollection- View содержит похожий конструктор: ListCollectionView Istcollvlew = new ListCol1ectionView(coll); Однако аргумент coll конструктора ListCollectionView должен реализовать ин- терфейс IList, который включает интерфейсы {Enumerable и {Collection (определяет свойство Count), а также определяет индексатор и методы Add и Remove. Стати- ческий метод CollectionViewSource.GetDefaultView возвращает CollectionView или List- CollectionView в зависимости от интерфейсов, реализованных аргументом. Объект CollectionView не позволяет сортировать и группировать свою коллекцию; объект ListCollectionView позволяет это сделать. Если объект CollectionView должен быть основан на ObservableCollection, вы мо- жете явно создать объект ListCollectionView, потому что ObservableCollection поддер- живает интерфейс IList. Логика управления текущей записью в CollectionView реализуется нескольки- ми свойствами и методами. Свойства Count, Currentitem и Currentposition доступ- ны только для чтения. Для изменения текущей позиции используются методы MoveCurrentTo, MoveCurrentToPosition, MoveCurrentToFirst, MoveCurrentToLast, Move- CurrentToPrevious и MoveCurrentToNext. Класс разрешает переместиться к позиции, на 1 меньшей индекса первого элемента и на 1 большей индекса последнего эле- мента; свойства IsCurrentBeforeFirst и IsCurrentAfterLast сообщают об этих особых случаях. CollectionView также определяет события CollectionChanged, PropertyChanged, Currentchanging и CurrentChanged.
756 Глава 26. Ввод и представления данных Давайте переработаем интерфейс навигации в контексте класса CollectionView В .NET Framework 2.0 присутствует новый элемент Windows Forms с именем BindingNavigator, который выводит графические кнопки для перемещения по базе данных, а также создания и удаления записей. Я попытался воспроизвести функ- циональность BindingNavigator в следующем элементе NavigationBar. NavigationBar.xaml <I_ _ ================================================ NavigationBar.xaml (с) 2006 by Charles Petzold <ToolBar xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Cl ass="Petzold.DataEntry.NavigationBar"> <Button Click="FirstOnClick" ToolTip="Move first"> <Image Source="DataContainer_MoveFirstHS.png" Stretch="None" /> </Button> <Button Name="btnPrev" Click="PreviousOnClick" ToolTip="Move previous"> <Image Source="DataContainer_MovePreviousHS.png" Stretch="None" /> </Button> Separator /> <TextBox Name="txtboxCurrent" Width="48" ToolTip="Current position" GotKeyboardFocus="TextBoxOnGotFocus" LostKeyboardFocus="TextBoxOnLostFocus" KeyDown="TextBoxOnKeyDown" /> <TextBlock Text="of " Vertical Alignment="Center" /> <TextBlock Name="txtblkTotar’ Text="0" VerticalAlignment="Center" ToolTip="Total number of iterns"/> Separator /> <Button Name="btnNext" Click="NextOnClick" ToolTip="Move next"> <Image Source="DataContainer_MoveNextHS.png" Stretch="None" /> </Button> <Button Click="LastOnClick" ToolTip="Move last"> <Image Source="DataContainer_MoveLastHS.png" Stretch="None" /> </Button> Separator /> <Button Click="AddOnClick" ToolTip="Add new"> <Image Source="DataContainer_NewRecordHS.png" Stretch="None" /> </Button> <Button Name="btnDel" Click="DeleteOnClick" ToolTip="Delete"> <Image Source="DeleteHS.png" Stretch="None" /> </Button> </ToolBar> PNG-файлы были взяты из коллекции изображений, входящей в поставку Visual Studio. Файл программной логики класса NavigationBar определяет два
ряод ипредставления данных 757 свойства. Первое свойство Collection относится к типу IList. (Предполагается, что ему будет задан объект ObservableCollection.) Метод доступа set использует стати- ческий метод CollectionViewSource.GetDefaultView для создания объекта CollectionView, который в действительности будет объектом ListCollectionView. Затем метод set ус- танавливает обработчики событий, обеспечивающие обновление навигационных элементов. Второе свойство ItemType задает тип объектов, хранящихся в коллекции. Он необходим элементу NavigationBar для реализации кнопки Add New. Остальная логика имеет много общего с логикой кнопок навигации, приведенной ранее. Кроме того, NavigationBar разрешает пользователю вручную ввести номер записи, а три обработчика событий TextBox в конце файла проверяют, чтобы это число находилось в правильном диапазоне. NavigationBar.cs //----------------------------------------------- И NavigationBar.cs (с) 2006 by Charles Petzold И------------------------------------------------ using System; using System.Col lections; // for IList. using System.Col lections.Special!zed; //Для NotifyCollectionChangedEventArgs using System.ComponentModel; // Для ICollectionView using System.Reflection; // Для Constructorinfo. using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; namespace Petzold.DataEntry { public partial class NavigationBar : ToolBar { IList coll; ICollectionView collview; Type typeItern; // Открытый конструктор public NavigationBarO { Initiali zeComponent(); } // Открытые свойства public IList Collection { set { coll = value; // Создание CollectionView и установка обработчиков событий collview = Col 1ectionViewSource.GetDefaultView(col1);
758 Глава 26. Ввод и представления данных col 1 view.CurrentChanged += Col 1ectionVIewOnCurrentChanged: col 1 view.Col 1ectionChanged += CollectionViewOnCollectionChanged: // Вызов обработчика событий для инициализации // элементов TextBox и Button CollectionViewOnCurrentChanged(nul1, null); // Инициализация TextBlock txtblkTotal .Text = coll .Count.ToStringO: get { return coll; } } // Тип элементов коллекции. // Используется в команде Add public Tyoe ItemTyoe { set { typeItem = value: } get { return typeItern; } } // Обработчики событий CollectionView void CollectionViewOnCollectionChanged(object sender. NotifyCollectionChangedEventArgs args) txtblkTota I .Text = coll .Count.ToStringO : } void CollectionViewOnCurrentChangedlobject sender. EventArgs args) { txtboxCurrent.Text = (1 + col 1 view.CurrentPosition),ToString(); btnPrev.IsEnabled = col 1 view.CurrentPosition > 0: btnNext.IsEnabled = coll view.CurrentPosition < coll.Count - 1: btnDel.IsEnabled = coll.Count > 1; } // Обработчики событий кнопок void FirstOnClick(object sender, RoutedEventArgs args) { col 1 view.MoveCurrentToFi rst(): } void PreviousOnClick(object sender, RoutedEventArgs args) { col 1view.MoveCurrentToPrevious(); } void NextOnClick(object sender, RoutedEventArgs args) { col 1vi ew.MoveCurrentToNext(); } void LastOnClick(object sender, RoutedEventArgs args) {
ВвОд И представления данных 759 collview.MoveCurrentToLast(); } void AddOnClick(object sender. RoutedEventArgs args) { Constructorinfo info = typeItem.GetConstructor(System.Type.EmptyTypes); coll.Add(info.Invoke(null)); col 1 view.MoveCurrentToLast(); } void DeleteOnClick(object sender, RoutedEventArgs args) { col 1.RemoveAt(col 1vi ew.CurrentPosi ti on): } // Обработчики событий txtboxCurrent string strOriginal; void TextBoxOnGotFocus(object sender, KeyboardFocusChangedEventArgs args) { strOriginal = txtboxCurrent.Text; } void TextBoxOnLostFocus(object sender. KeyboardFocusChangedEventArgs args) { int current; if (Int32.TryParse(txtboxCurrent.Text, out current)) if (current > 0 && current <= coll.Count) coll view.MoveCurrentToPosition(current - 1): else txtboxCurrent.Text = strOriginal; } void TextBoxOnKeyDown(object sender, KeyEventArgs args) { if (args.Key == Key.Escape) { txtboxCurrent.Text = strOriginal; args.Handled = true; } else if (args.Key == Key.Enter) { args.Handled = true; } else return; MoveFocus(new TraversalRequest(FocusNavigationDirection.Right));
760 Глава 26. Ввод и представления данных Два файла, составляющих класс NavigationBar, и необходимые графические файлы являются частью проекта Petzold.DataEntry, создающего библиотеку ди. намической компоновки с именем Petzold.DataEntry.dll. Этот проект вместе с про- ектом DataEntryWithNavigation входит в решение DataEntryWithNavigation. Проект DataEntryWithNavigation содержит ссылки на файлы Person.cs и Ре- ople.cs, а также на файлы класса DatePicker. Кроме того, в проект входят XAML- файл и файл C# для окна программы. В XAML-файле определяется элемент Window с DockPanel. Знакомое меню пристыковано у верхнего края, полоса NavigationBar — внизу, а панель PersonPanel заполняет внутреннюю область. DataEntryWithNavigation.xaml <।_ _ =^======================================================== DataEntryWithNavigation.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:pnl="cl r-namespace:Petzold.Si ngleRecordDataEntry" xml ns:nav="cl r-namespace:Petzold.DataEntry;assembly=Petzold.DataEntry" x:Cl ass="Petzold.DataEntryWithNavigation.DataEntryWithNavigation" Title="Data Entry with Navigation" Si zeToContent="Wi dthAndHei ght" Resi zeMode="CanMi nimi ze"> <DockPanel Name="dock"> <Menu DockPanel.Dock="Top"> <MenuItem Header="_File"> <MenuItem Header="_New" Command="New" /> <MenuItem Header="_Open..." Command="Open" /> <MenuItem Header="_Save..." Command="Save" /> </MenuItem> </Menu> <!-- Элемент NavigationBar для навигации --> <nav:NavigationBar Name="navbar" DockPanel.Dock="Bottom" /> <!-- Панель PersonPanel для отображения и ввода данных --> <pnl:PersonPanel x:Name="pnlPerson" /> </DockPanel> <Wi ndow.CommandBi ndi ngs> <CommandBinding Command="New" Executed="NewOnExecuted" /> <CommandBinding Command="Open" Executed="OpenOnExecuted" /> <CommandBinding Command="Save" Executed="SaveOnExecuted" /> </Window.CommandBi ndi ngs> </Window> Файл программной логики становится довольно тривиальным. Как и в исход- ном проекте SingleRecordDataEntry, в нем необходимо реализовать лишь обра-
0ВОд и представления данных 761 ботчики событий трех команд меню. Для команд New и Open обработчик события вызывает метод InitializeNewPeopleObject, связывающий объект People с элемента- ми PersonPanel и NavigationBar. pataEntryWithNavigation.cs //- // DataEntryWithNavigation.cs (с) 2006 by Charles Petzold //- using Petzold.DataEntry; using Petzold.MuitiRecordDataEntry; using Petzold.SingleRecordDataEntry; using System; using System.Collections.Spec!a1ized; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.DataEntryWithNavigation public partial class DataEntryWithNavigation { People people; [STAThread] public static void MainO { Application app = new Application); app.Run(new DataEntryWithNavigationO); i j public DataEntryWithNavigation() { InitializeComponent(); // Имитация команды File>New ApplicationCommands.New.Execute(null, this); // Передача фокуса первому элементу TextBox на панели pnlPerson.Chi 1dren[1].Focus(); } // Обработчики событий для команд меню void NewOnExecuted(object sender, ExecutedRoutedEventArgs args) { people = new People(); people. Add (new PersonO); Initial!zeNewPeop1eObj ect(); } void OpenOnExecuted(object sender, ExecutedRoutedEventArgs args)
762 Глава 26. Ввод и представления данных people = People.Load(this); Initial!zeNewPeopleObj ect(); } void SaveOnExecuted(object sender, ExecutedRoutedEventArgs args) { people.Save(this): } void InitializeNewPeopleObject() { navbar.Col lection = people; navbar.ItemType = typeof(Person); pnlPerson.DataContext = people: } } } Метод InitializeNewPeopleObject сначала присваивает свойству Collection объекта NavigationBar созданный объект People. Свойство Collection создает новый объект типа ListCollectionView и устанавливает обработчики событий, обеспечивающие работу навигационных элементов. Вторая команда InitializeNewPeopleObject задает свойство ItemType объекта Navi- gationBar, чтобы объект NavigationBar мог создавать новые объекты и включать их в коллекцию, когда пользователь щелкает на кнопке Add New. Последняя команда InitializeNewPeopleObject задает свойству DataContext объек- та PersonPanel объект People. На первый взгляд это выглядит несколько странно. В предыдущих программах свойству DataContext объекта PersonPanel присваивал- ся объект Person, чтобы на панели выводились все свойства, определяемые клас- сом Person. Связывание DataContext с объектом People выглядит не совсем логично, но работает. Коллекция People взята за основу для создания объекта ListCollection- View, поддерживающего текущую запись, а эта текущая запись — именно то, что выводится на PersonPanel. Возможно, объект NavigationBar хорошо подходит для некоторых программ ввода данных, однако его возможностей недостаточно в тех ситуациях, когда пользователю требуется общее представление обо всем массиве данных. Напри- мер, ему мог бы пригодиться список ListBox для прокрутки базы данных и выбора записей для просмотра/рсдактпрования. Такая возможность реализуется гораз- до проще, чем кажется; это показано в следующем проекте, который называется DataEntryWithListBox. Проекту необходимы файлы, обеспечивающие работу классов Person, People, PersonPanel и DatePicker. Следующий XAML-файл опреде- ляет окно со знакомым меню, элементом ListBox и панелью PersonPanel. DataEntryWithListBox.xaml <I_ _ ======================================================= DataEntryWithListBox.xaml (с) 2006 by Charles Petzold ======================================================= _ <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Ввод и представления данных 763 xml ns:pnl="clr-namespace:Petzold.SIngleRecordDataEntry" x:Class="Petzold.DataEntryWIthListBox.Da taEntryWIthListBox" T1tle="Data Entry With ListBox" S1zeToContent="W1dthAndHe1ght" ReslzeMode="CanM1nimlze"> <DockPanel Name="dock"> <Menu DockPanel.Dock="Top"> <MenuItem Header="_F11e"> <MenuItem Header="_New" Command="New" /> <MenuItem Header="_Open..." Command="Open" /> <MenuItem Header="_Save..." Command="Save" /> </MenuItem> </Menu> <Gr1d> Crld. RowDefi n1t1ons> <RowDef1n1t1on He1ght="Auto" /> <RowDef1n1t1on He1ght="Auto" /> </Gr1d.RowDefinit1ons> <Gr1d.ColumnDef1nitions> ColumnDefinition W1dth="Auto" /> ColumnDefinition W1dth="Auto" /> </Gr1d.Co IumnDef1ni11ons> <!-- Элемент ListBox для вывода и выбора записей --> <L1stBox Name="Istbox" Grid.Column="0" W1dth="300" He1ght="300" Marg1n="24"> CIstBox. ItemTemplate> <DataTemplate> CtackPanel 0r1entat1on="Hor1zontal "> <TextBlock Text="{B1nd1ng Path=F1rstName}" /> <TextBlock Text=" " /> <TextBlock Text="{B1nd1ng Path=M1ddleName}" /> <TextBlock Text=" " Name=,,txtblkSpace"/> <TextBlock Text="{B1nd1ng Path=LastName}" /> <TextBlock Text-" (" /> <TextBlock Text-"{Binding Path=B1rthDate.Year}" /> <TextBlock Text-"-" /> <TextBlock Text-"{Binding Path=DeathDate.Year}" Name="txtblkDeath" /> <TextBlock 7ext=")" /> </StackPanel> <DataTemplate.Tr1ggers> <DataTr1gger Bindlng="{Blndlng Path-DeathDate}" Value="{x:Null}"> Cetter TargetName-"rxtblkDeath" Property-"Text" Value="present" /> </DataTrigger>
764 Глава 26. Ввод и представления данных <DataTr1gger Binding^'{Binding Path=M1ddleName}" Value=""> <Setter TargetName="txtblkSpace" Property="Text" Value="" /> </DataTr1gger> </DataTemplate.Triggers> </DataTemplate> </L1stBox.ItemTemplate> </L1stBox> <!-- Панель PersonPanel для ввода и редактирования записей --> <pnl:PersonPanel x:Name="pn1 Person" Grid.Columnar /> <!-- Кнопки добавления и удаления записей --> <StackPane1 0r1entat1on="Hor1zontal" Gr1d.Row='T’ Grid.Column="l"> <Button Marg1n="12" C11ck="AddOnC11ck"> Add new Item </Button> <Button Marg1n="12" C11ck="Delete0nC11ck"> Delete Item </Button> </StackPanel> </Gr1d> </DockPanel> <W1ndow.CommandBindings> <CommandB1nd1ng Command="New" Executed="NewOnExecuted" /> <CommandB1nd1ng Command="Open" Executed="OpenOnExecuted" /> <CommandB1nd1ng Command="Save" Executed="SaveOnExecuted" /> </W1ndow.CommandBindings> </W1ndow> Свойство ItemTemplate объекта ListBox определяется в предположении, что спи- сок будет использоваться для отображения данных типа Person. Панель StackPanel с горизонтальной ориентацией объединяет десять элементов TextBlock, обеспечи- вающих вывод данных в формате Franz Schubert (1797-1828) За элементом StackPanel следует секция DataTemplate.Triggers с двумя элемен- тами DataTrlgger. Эти элементы решают две проблемы: во-первых, если свойство DeathDate равно null, вместо свойства Year объекта DeathDate должно выводиться слово present, чтобы запись выглядела так: John Adams (1947-present) Без элемента DataTrlgger за дефисом никакие символы не выводятся (что так- же является приемлемым способом отображения информации). Второй элемент DataTrlgger решает проблему, которая возникает при отсутствии второго имени. Обычно один пробел отделяет имя от второго имени, а второй пробел отделяет
Ввод и представления данных 765 второе имя от фамилии. Если второго имени нет, имя будет отделено от фами- лии двумя пробелами вместо одного. Второй элемент DataTrigger удаляет один из пробелов. Макет окна завершается элементом PersonPanel и кнопками добавления/уда- ления записей. Кнопки навигации не нужны, потому что запись для просмот- ра/редактирования выбирается в списке ListBox. Далее приводится содержимое файла программной логики. DataEntryWithListBox.cs //----------------------------------------------------- И DataEntryWithListBox.cs (с) 2006 by Charles Petzold //----------------------------------------------------- using Petzold.MuitiRecordDataEntry; using Petzold.Si ngleRecordDataEntry; using System: using System.Col 1ections.Specialized: using System.ComponentModel: using System.Windows; using System.Windows.Controls: using System.Windows.Data: using System.Windows.Input; using System.Windows.Media; namespace Petzold.DataEntryWithLi stBox public partial class DataEntryWithListBox { ListCollectionView coll view: People people: [STAThread] public static void MainO { Application app = new Application): app.Run(new DataEntryWithListBox()): } public DataEntryWithListBoxO Initial!zeComponent(): // Имитация команды F11e>New Appl1 cat1onCommands.New.Execute(nul1, this): // Передача фокуса первому элементу TextBox на панели pnlPerson.Children[l].Focus (): } void NewOnExecuted(object sender. ExecutedRoutedEventArgs args) { people = new PeopleO: people. Add (new PersonO):
766 Глава 26. Ввод и представления данных Initial izeNewPeopleObject(); } void OpenOnExecuted(object sender. ExecutedRoutedEventArgs args) people = People.Load(this): If (people != null) Initial!zeNewPeopleObject(); void SaveOnExecuted(object sender. ExecutedRoutedEventArgs args) { people.Save(this): void InitialIzeNewPeopleObject() { // Создание объекта ListCollectionView на базе объекта People collview = new ListCollectionView(people): // Определение свойства для сортировки представления col 1vj ew.SortDescri ptj ons.Add( new SortDescriptionC'LastName". ListSortDirection.Ascending)); // Связывание ListBox и PersonPanel через ListCollectionView Istbox.ItemsSource = coll view; pnlPerson.DataContext = collview; // Задание индекса выделенной строки ListBox if (Istbox.Items.Count > 0) Istbox.Selectedlndex = 0; } void AddOnClick(object sender. RoutedEventArgs args) { Person person = new PersonO; people.Add(person); Istbox.Selectedltem = person; pnlPerson.Chi 1dren[1].Eocus(); coll view.RefreshO; } void DeleteOnClick(object sender, RoutedEventArgs args) { if (Istbox.Selectedltem != null) people.Remove(Istbox.Selectedltem as Person); if (Istbox.Iterns.Count > 0) Istbox.Selectedlndex = 0; else AddOnClick(sender, args);
ВВОД и представления данных 767 Команды New и Open меню File вызывают InitializeNewPeopleObject. Метод со- здает на основе объекта People объект ListCollectionView. Как было указано ранее, объект CollectionView является представлением кол- лекции. Он предоставляет возможность упорядочения элементов коллекции для просмотра, но представление не изменяет в базовой коллекции. Класс Collection- View поддерживает три основных механизма изменения представлений: О сортировка; Q фильтрация; О группировка. Сортировка данных выполняется но одному пли нескольким свойствам, фильтрацией называется ограничение, при котором просматриваются только записи, удовлетворяющие определенным критериям. При группировке записи распределяются по группам, также основанным на заранее заданных критериях. CollectionView содержит’ три свойства с именами CanSort, CanFilter и CanGroup. Свой- ства CanSort и CanGroup ложны для объекта CollectionView, но истинны для объек- та ListCollectionView. Чтобы отсортировать объект ListCollectionView, следует создать объект типа Sort- Description и включить его в коллекцию SortDescriptions. Вот как в программе Data- EntryWithListBox объект ListCollectionView сортируется по свойству LastName: coll view.SortDescrlptlons.Add( new SortDescription("LastName". ListSortDirection.Ascending)): Коллекция SortDescriptions может содержать несколько объектов SortDescription. Второй объект SortDescription используется при совпадении двух свойств, описы- ваемых первым объектом SortDescription. Свойство SortDescriptions, определяемое CollectionView, относится к типу SortDescriptionCollection и содержит методы Setitem, Insertitem, Removeitem и Clearltems для выполнения операций с коллекцией. После определения SortDescription метод InitializeNewPeopleObject задает объект ListCollectionView свойству ItemsSource элемента ListBox: 1stbox.ItemsSource = coll view; Теперь ListBox выводит все записи коллекции отсортированными по фамилии. Впрочем, это еще не все: ListBox связывается с CollectionView так, что свойство Currentitem объекта CollectionView задается по свойству Selectedltem объекта ListBox. Элемент ListBox становится средством навигации для коллекции. Это означа- ет, что свойству DataContext панели PersonPanel можно присвоить тот же объект CollectionView: pnlPerson.DataContext = coll view; Теперь на панели PersonPanel выводится запись, выбранная в ListBox, а панель может использоваться для редактирования записи. События PropertyChanged, оп- ределяемые классами Person, ObservableCollection и CollectionView, обеспечивают об- новление ListView с каждым изменением. Разобраться в оставшемся коде программы несложно. Обработчик события кнопки Add добавляет новый объект Person в класс People. Новый объект Person
768 Глава 26. Ввод и представления данных автоматически отображается в конце списка. Далее обработчик события выде- ляет новый объект в списке ListBox, в результате чего его свойства отображаются на панели PersonPanel. Данные, вводимые на панели, не перемещаются автоматически на новую по- зицию ListBox в результате описания сортировки. Чтобы заново отсортировать содержимое CollectionView с учетом новых или измененных данных, программа должна вызвать метод Refresh. Конкретный момент вызова Refresh зависит от архитектуры программы. В некоторых программах пользователь вводит новую запись и щелкает на кнопке Add. В этом случае можно вызвать Refresh после до- бавления новой записи в коллекцию. В программе DataEntryWithListBox нет кнопки, сигнализирующей о заверше- нии ввода. Вместо этого программа вызывает Refresh в конце метода AddOnClick чтобы заново отсортировать коллекцию со всеми ранее добавленными запися- ми. Добавленная запись Person оказывается в начале списка, потому что строка по умолчанию «<last пате>» предшествует любой последовательности алфа- витных символов. Конечно, в программу можно добавить интерфейс сортировки по разным полям — это один из способов упрощения поиска. Другой способ называется фильтрацией — в объект CollectionView включаются только те элементы коллек- ции, которые удовлетворяют определенному критерию. Для фильтрации CollectionView необходимо иметь метод, определенный в со- ответствии с делегатом Predicate: bool MyFiIter(object obj) { } В аргументе метода передается объект коллекции. Метод возвращает true, если объект должен быть включен в представление, или false в обратном случае. Метод задается свойству Filter объекта CollectionView: coll view.Filter = MyFilter; Взглянув на определение делегата Predicate, вы увидите, что это шаблонный делегат, определяемый следующим образом: public delegate bool Predicate<T> (T obj) Возникает впечатление, что метод фильтрации можно определить с аргумен- том любого типа. Тем не менее определение свойства Filter в классе CollectionView требует метода типа Predicate<Object>, поэтому метод фильтрации должен иметь аргумент типа object. Программа CollectionViewWithFilter содержит ссылки на файлы Person.cs и People.cs. При определении окна в ней используются как файлы XAML, так и файлы С#. Окно содержит три переключателя для выбора режима выбора: живые, умершие или все композиторы. Далее приводится XAML-макет пере- ключателей и элемента ListBox.
Ввод И представления данных 769 Collection View With Filter.ха ml <! - - =========================================================== CollectionVIewWIthFIlter.xaml (c) 2006 by Charles Petzold Window xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml" x:Class="Petzold.Col 1ectionVIewWIthFIIter.Col 1ectionVIewWIthFIIter" T1tle="Collect1onV1ew with Filter" SIzeToContent="W1dthAndHelght" ResizeMode="CanM1nimlze"> <DockPanel Name="dock"> <Menu DockPanel.Dock="Top"> <MenuItem Header="_F11e"> <MenuItem Header="_Open..." Command="Open" /> </MenuItem> </Menu> <Gr1d> <Gr1d.RowDef1nit1ons> <RowDef1n1t1on He1ght="Auto" /> <RowDef1n1t1on He1ght="Auto" /> </Gr1d.RowDef1nitions> <!-- GroupBox с элементами RadioButtons для фильтрации --> <GroupBox Gr1d.Row="0" Marg1n="24" HorizontalAl1gnment="Center" Header="Cr1ter1a"> <StackPanel> <Rad1oButton Name="rad1oL1vlng" Content="L1v1ng" Checked="Rad1o0nChecked" Marg1n="6" /> <Rad1oButton Name="rad1oDead" Content="Dead" Checked="Rad1oOnChecked" Marg1n="6" /> <Rad1oButton Name="radloAl1" Content="Al 1" Checked="Rad1o0nChecked" Marg1n="6" /> </StackPanel> </GroupBox> <!-- Элемент ListBox для отображения записей --> <L1stBox Name="lstbox" Gr1d.Row="l" Hor1zontalA11gnment="Center" W1dth="300" He1ght="300" Marg1n="24"> <L1stBox.ItemTemplate> <DataTemplate> <StackPanel 0r1entat1on="Hor1zontal"> <TextBlock Text="{B1nd1ng Path=F1rstName}" /> <TextBlock Text=" " /> <TextBlock Text="{B1nd1ng Path=M1ddleName}" /> <TextBlock Text=" " Name="txtblkSpace"/> <TextBlock Text="{B1nd1ng Path=LastName}" /> <TextBlock Text=" (" />
770 Глава 26. Ввод и представления данных <TextBlock Text="{Binding Path=B1rthDate.Year}" /> <TextBlock Text="-" /> <TextBlock Text="{B1nding Path=DeathDate.Year}" Name="txtblkDeath" /> <TextBlock Text=")" /> </StackPane1> <DataTemplate.Tri ggers> <DataTrigger Blndlng=M{Blnding Path=DeathDate}" Value="{x:Null}"> <Setter TargetName="txtblkDeath" Property="Text" Vaiue="present" /> </DataTrigger> <DataTrigger B1ndjng="{Binding Path=M1ddleName}" Value=""> <Setter TargetName="txtblkSpace" Property="Text" Value="" /> </DataTrigger> </DataTemplate.Tr1ggers> </DataTemplate> </L1stBox.ItemTemplate> </L1stBox> </Grid> </DockPanel> <W1ndow.CommandBi ndlngs> <CommandB1nd1ng Command="Open" Executed="OpenOnExecuted" /> </W1ndow.CommandBindlngs> </W1ndow> Чтобы избежать лишнего усложнения программы, я не стал включать в нее панель PersonPanel или кнопки для добавления и удаления записей из коллек- ции. Программа просто позволяет просмотреть существующий файл с учетом текущего фильтра. Однако это вовсе не означает, что в режиме фильтрации вы теряете возможность ввода новых данных. В каталоге главы 26 архива примеров находится файл Composers.PeopleXml. Вы можете использовать его для экспериментов с этой программой, а также следую- щими двумя программами этой главы. Три элемента RadioButton используют один обработчик события, определяе- мый в файле программной логики. В зависимости от свойства Name переключате- ля он задает свойству Filter объекта CollectionView метод PersonlsLiving, PersonlsDead или null (режим вывода всех записей). CollectionViewWithFilter.cs //---------------------------------------------------------- // Collect1onV1ewWithF11ter.cs (с) 2006 by Charles Petzold //---------------------------------------------------------- using Petzold.SingleRecordDataEntry: using Petzold.MuitiRecordDataEntry;
Ввод и представления данных 771 using System; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; namespace Petzold.Col 1ectionVi ewWithFi1 ter public partial class CollectionViewWithFiIter : Window { ListCollectionView coll view; [STAThread] public static void MainO { Application app = new Application); app.Run(new CollectionViewWithFiIterO); } public CollectionViewWithFilter() { InitializeComponent(): } void OpenOnExecuted(object sender. ExecutedRoutedEventArgs args) { People people = People.Load(this); if (people != null) { coll view = new ListCollectionView(people); col 1 view.SortDescriptions.Add( new SortDescriptionCLastName". Li stSortDi recti on.Ascendi ng)); 1stbox.ItemsSource = coll view; if (1stbox.Items.Count > 0) 1stbox.Seiectedlndex = 0; radioAll.IsChecked = true; } } // Обработчики событий элементов RadioButton void RadioOnChecked(object sender, RoutedEventArgs args) { if (collview == null) return; RadioButton radio = args.Source as RadioButton; switch (radio.Name) { case "radioLiving": collview.Filter = PersonlsLiving;
772 Глава 26. Ввод и представления данных break; case "radloDead": collview.Filter = PersonlsDead; break; case "radioAll": collview.Filter = null; break; } } bool PersonlsLiving(object obj) { return (obj as Person).DeathOate == null: } bool PersonIsDead(object obj) { return (obj as Person).DeathOate != null; } } } В любой момент времени свойству Filter объекта CollectionView может быть за- дан только один метод. Если записи должны фильтроваться по нескольким кри- териям, вероятно, стоит ограничиться одним методом, который будет только один раз задан свойству Filter. Этот метод анализирует объект по критериям фильтрации и возвращает свое решение. При каждом изменении критериев про- грамма вызывает метод Refresh объекта CollectionView, чтобы все записи были за- ново обработаны методом фильтрации. Один из распространенных примеров применения фильтрации — поле Text- Box, в котором пользователь вводит одну или несколько букв. В списке ListBox отображаются все записи, у которых фамилия начинается с введенных букв. Фильтрация такого рода реализуется проще, чем может показаться на первый взгляд. Следующий XAML-файл напоминает CollectionViewWithFilter.xaml, но вме- сто GroupBox и переключателей в нем используются элементы Label и TextBox. Элемент TextBox содержит свойство Name со значением txtboxFilter и обработчик события TextChanged. FilterWithText.xaml <I_ _ ================================================= FilterWithText.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml /presentation" xml ns;x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Class="Petzold.Fi1terWi thText.Fi1terWi thText" Title=”Fi1 ter with Text" Si zeToContent="Wi dthAndHei ght" Resi zeMode="CanMi ni mi ze"> <DockPanel Name="dock">
Ввод и представления данных 773 <Menu DockPanel .Dock=,,Top"> <MenuItem Header="_F11e"> <MenuItem Header="_Open..." Command="Open" /> </MenuItem> </Menu> <Gr1d> <Grid.RowDef1niti ons> <RowDefinition He1ght="Auto" /> <RowDef1n1t1on He1ght="Auto" /> </Gr1d.RowDef1nit1ons> <!-- Элемент TextBox с элементом Label --> <StackPanel Gr1d.Row="0" Marg1n="24" 0r1entat1on=,,Horizonta 1" Horizontal Al1gnment="Center"> <Label Content^‘Search: ” /> <TextBox Name="txtboxF11ter" M1nW1dth="11n" TextChanged=,,TextBoxOnTextChanged,‘ /> </StackPanel> <!-- Элемент ListBox для вывода записей --> <L1stBox Name="lstbox" Gr1d.Row="r Horizontal Al1gnment="Center" Width="300" He1ght="300" Marg1n="24"> <L1stBox.ItemTemplate> <DataTemplate> <StackPanel 0r1entat1on=”Hor1zontal“> <TextBlock Text="{B1nd1ng Path=F1rstName}“ /> <TextBlock Text=" " /> <TextBlock Text="{B1nd1ng Path=M1ddleName}“ /> <TextBlock Text=" " Name="txtblkSpace,7> <TextBlock Text="{B1nd1ng Path=LastName}“ /> <TextBlock Text=" (” /> <TextBlock Text="{B1nd1ng Path=B1rthDate.Year}" /> <TextBlock Text=,,-“ /> <TextBlock Text="{B1nd1ng Path=DeathDate.Year}" Name="txtblkDeath" /> <TextBlock Text=")“ /> </StackPanel> <DataTemplate.Triggers> <DataTr1gger B1nd1ng="{B1nd1ng Path=DeathDate}“ Value="{x:Null}"> <Setter TargetName="txtblkDeath" Property=”Text" Value=”present” /> </DataTr1gger> <DataTr1gger B1nd1ng="{B1nd1ng Path=M1ddleName}“ Value=""> <Setter TargetName="txtblkSpace" Property="Text" Value=“" />
774 Глава 26. Ввод и представления данных </DataTr1gger> </DataTemp1 ate.Т riggers> </DataTemplate> </ListBox.ItemTemplate> </L1stBox> </Grid> </DockPanel> <Window.CommandBindings> <CommandB1nd1ng Command=,,Open” Executed=,,OpenOnExecuted" /> </W1ndow.CommandBindings> </W1ndow> Файл программной логики класса FilterWithText готовит окно к выводу нового объекта CollectionView; для этого свойству Text объекта TextBox задается пустая строка, а свойству Filter — метод LastNameFilter: txtboxFi 1 ter. Text = ,,,,; colIview.Filter = LastNameFilter; Метод LastNameFilter и обработчик события TextChanged находятся в конце файла. FilterWithText.cs // // FilterWithText.cs (с) 2006 by Charles Petzold // using Petzold.SIngleRecordDataEntry; using Petzold.MuitiRecordDataEntry; using System; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; namespace Petzold.FilterWithText { public partial class FilterWithText : Window { ListCollectionView collview; [STAThread] public static void MainO { Application app = new Application); app.Run(new FIlterWIthTextO); } public FIlterWIthTextO
Ввод и представления данных 775 { InitiallzeComponent(); } void OpenOnExecuted(object sender, ExecutedRoutedEventArgs args) { People people = People.Load(this); If (people != null) { collview = new LlstCollectlonVlew(people); col 1view.SortDescr1 pt 1 ons.Add( new SortDescrlptlonC’BIrthDate”. LIstSortDIrecti on.Ascendlng)): txtboxFIIter.Text = collvlew.Filter = LastNameFIlter; Istbox.ItemsSource = collvlew; If (Istbox.Iterns.Count > 0) Istbox.Selectedlndex = 0: } } bool LastNameF11ter(object obj) { return (obj as Person).LastName.StartsW1th(txtboxF11ter.Text, St r1ngCompar1 son.CurrentCul turelgnoreCase); } void TextBoxOnTextChanged(object sender, TextChangedEventArgs args) { If (collvlew == null) return; coll view.RefreshO; } } } Метод LastNameFIlter возвращает true, если свойство LastName объекта Person начинается с текста, содержащегося в TextBox (без учета регистра символов). При любых изменениях в TextBox вызывается метод Refresh, заново обрабатывающий все содержимое коллекции. Кроме сортировки и фильтрации, класс CollectionView поддерживает группи- ровку объектов. Данная возможность продемонстрирована в последней програм- ме этой главы. Чтобы сгруппировать записи, необходимо передать метод, присваивающий каждому объекту в коллекции имя (тоже объект). Содержимое ListBox группиру- ется по именам, но вы также можете предоставить заголовки и панели для раз- деления групп. CollectionView содержит свойство GroupDescrlptions, которое относится к типу ObservableCollectlon<GroupDescrlptlon>. GroupDescriptlon — абстрактный класс с од- ним производным классом PropertyGroupDescriptlon, который может использоваться
776 Глава 26. Ввод и представления данных для группировки объектов коллекции по свойствам. В других случаях приходит- ся определять собственный класс, производный от GroupDescription, с обязатель- ным переопределением метода GroupNameFromltem. Я решил сгруппировать композиторов из файла примера по историческим периодам. Поскольку в моем файле не хранится информация о том, в каком пе- риоде работал тот или иной композитор, в качестве критерия был выбран год рождения. Класс PeriodGroupDescription, производный от GroupDescription, переоп- ределяет метод GroupNameFromltem. Переопределенный метод возвращает тексто- вую строку для каждого объекта коллекции. PeriodGroupDescription.cs // // PeriodGroupDescript1on.cs (с) 2006 by Charles Petzold // using Petzold.SingleRecordDataEntry; using System; using System.ComponentModel; using System.Globalization; namespace Petzold.Li stBoxWithGroups { public class PeriodGroupDescription : GroupDescription { public override object GroupNameFromItem(object item, int level, Cultureinfo culture) { Person person = item as Person; if (person.BirthDate == null) return "Unknown"; int year = ((DateTime)person.BirthDate).Year; if (year < 1575) return "Pre-Baroque"; if (year < 1725) return "Baroque"; if (year < 1795) return "Classical"; if (year < 1870) return "Romantic"; if (year < 1910) return "20th Century"; return "Post-War"; } } } Рассмотрение класса окна лучше начать с кода С#.
Ввод и представления данных 777 ListBoxWithGroups.cs // // ListBoxW1thGroups.cs (с) 2006 by Charles Petzold и using Petzold.MuitiRecordDataEntry; using System: using System.ComponentModel: using System.Windows: using System.Windows.Controls: using System.Windows.Data: using System.Windows.Input: using System.Windows.Media; namespace Petzold.ListBoxWithGroups { public partial class ListBoxWithGroups : Window { ListCollectionView collvlew: [STAThread] public static void MainO { Application app = new Application): app.Run(new ListBoxWithGroups()): } public ListBoxWithGroups() { InitializeComponent(); } void OpenOnExecuted(object sender, ExecutedRoutedEventArgs args) { People people = People.Load(this): if (people != null) { collview = new ListCollectionView(people): collvlew.SortDescr1 pt1ons.Add( new SortDescriptionCBirthDate", Li stSortDirecti on.Ascendi ng)); // Включение объекта PeriodGroupsDescription // в коллекцию GroupsDescriptions collvlew.GroupDescr1ptlons.Add( new PeriodGroupDescriptionO): Istbox.ItemsSource = collvlew: if (Istbox.Iterns.Count > 0) Istbox.Selectedlndex = 0: } } }
778 Глава 26. Ввод и представления данных Обработчик события OpenOnExecuted загружает существующий файл People и создает по нему объект ListCollectionView. Представление сортируется по дате рождения, после чего в коллекцию GroupDescriptions добавляется новый объект типа PeriodGroupDescription, обеспечивающий группировку данных по критерию этого класса. XAML-файл окна программы содержит уже знакомый элемент ListBox, но на- ряду с ним также присутствует элемент свойства ListBox.GroupStyle. ListBoxWithGroups.xaml <I_ _ ==================================================== ListBoxWithGroups.xaml (с) 2006 by Charles Petzold <Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x=”http://schemas.microsoft.com/wi nfx/2006/xaml" x:Class="Petzold.Li stBoxWi thGroups.Li stBoxWi thGroups" Title="L1stBox with Groups" Si zeToContent="Wi dthAndHei ght" Resi zeMode="CanM1ni mi ze"> <DockPanel Name="dock"> <Menu DockPanel.Dock="Top"> <MenuItem Header="_File,'> <MenuItem Header="_Open..." Command=,,Open" /> </MenuItem> </Menu> <!-- Элемент ListBox для вывода записей --> <L1 stBox Name="Istbox" Gr1d.Row=,,l" HorizontalAlignment^'Center" VerticalAlignment^’Center" Width="300" Height=,,300" Margin=,,24"> < L1stBox.ItemTemplate> <DataTemplate> <StackPanel 0r1entat1on="Horizontal"> <TextBlock Text="{Binding Path=F1rstName}" /> <TextBlock Text=" " /> <TextBlock Text="{B1nding Path=M1ddleName}" /> <TextBlock Text=" " Name="txtblkSpace,7> <TextBlock Text="{B1nding Path=LastName}" /> <TextBlock Text=" (" /> <TextBlock Text="{B1nding Path=B1rthDate.Year}" /> <TextBlock Text="-" /> <TextBlock Text="{B1nding Path=DeathDate.Year}" Name=,,txtblkDeath" /> <TextBlock Text=")" /> </StackPanel> <DataTemplate.Tr1ggers> <DataTrigger Bindings’{Binding Path=DeathDate.Year}" Value="{x:Null}"> <Setter TargetName="txtblkDeath" Property="Text" Value="present" /> </DataTrigger>
Ввод и представления данных 779 <DataTr1gger Binding="{Binding Path=MiddleName}" Value=””> <Setter TargetName=,,txtblkSpace" Property=,,Text" Value="" /> </DataTrigger> </DataTemplate.Tri ggers> </DataTemplate> < /L1stBox.ItemTemplate> < !-- Элемент GroupStyle определяет заголовки групп --> <ListBox.GroupStyle> <GroupStyle> <GroupStyle.HeaderTemplate> <DataTemplate> <TextBlock Text="{Binding Path=Name}" Foreground="White" Background=,,DarkGray" FontWeight="Bold” Fontsize=”12pt” Margin=,,6" /> </DataTemplate> </GroupStyle.HeaderTempl ate> </GroupStyle> < /L istBox.GroupSty1e> </ListBox> </DockPanel> <Window.CommandBIndi ngs> <CommandBinding Command=,,Open" Executed="OpenOnExecuted" /> </Window.CommandBIndi ngs> </Window> Свойство GroupStyle элемента ListBox содержит коллекцию объектов типа Group- Style. Если коллекция содержит более одного объекта, вы также должны задать свойству GroupStyleSelector объекта ListBox метод, определяемый в соответствии с де- легатом GroupStyleSelector. Метод получает имя группы и возвращает GroupStyle для этого имени. Если определен только один объект GroupStyle, он применяется ко всем груп- пам, отображаемым в ListBox. Каждая группа может обладать собственным сти- лем (которое задается при помощи свойства Containerstyle), собственным типом панели для отображения элементов группы (задается свойством Panel) и шабло- ном заголовка HeaderTemplate. Последнее свойство было использовано мной для определения объекта TextBlock, отображающего белый текст на темно-сером фоне. Элемент Text содержит привязку к свойству Name, то есть к имени группы. Та- ким образом, каждой группе предшествует невыделяемый заголовок, описываю- щий период, к которому относятся композиторы данной группы. Механизм группировки, реализованный в ItemsControl, открывает принципи- ально новую перспективу отображения данных в ListBox. Возможно, для него не найдется немедленного применения в вашей повседневной работе. Но когда вы столкнетесь с задачей, требующей группировки данных в ListBox, вы по достоин- ству оцените возможности группировки.
27 Графические фигуры Двумерная компьютерная графика приблизительно делится на растровую и век- торную (линии, кривые, заполненные области). Впрочем, ключевым словом в этом утверждении является «приблизительно»: между двумя областями существуют значительные перекрытия. К какой категории отнести эллипс, закрашенный ки- стью, созданной на основе растрового изображения, — растровой или векторной? А если растровое изображение строится на базе векторного рисунка? В них при- сутствуют признаки обеих категорий. До настоящего момента мы рассматривали примеры использования элемента Image для вывода растровых изображений и различных классов из пространст- ва имен System.Windows.Shapes (также называемого библиотекой Shapes). Если ваши приложения умеренно используют графику, возможно, ничего больше вам не потребуется. Однако эти высокоуровневые классы являются лишь «верши- ной айсберга» графических возможностей WPF. В оставшихся главах мы иссле- дуем графику (а также анимацию) WPF более детально, вплоть до объединения растровой и векторной графики при рисовании и построении кистей. В этой главе библиотека Shapes будет описана более подробно, чем это дела- лось ранее. Эта библиотека удобна прежде всего тем, что ее классы являются производными от FrameworkElement, как показывает следующая иерархия классов: Object Dispatcherobject (абстрактный) Dependencyobject Visual (абстрактный) UIEIement FrameworkElement Shape (абстрактный) Ellipse Line Path Polygon Polyline Rectangle В результате наследования графические объекты, созданные на базе клас- сов Shape, воспроизводятся на экране и обрабатывают ввод с клавиатуры, мыши и стилуса, как обычные элементы управления.
Графические фигуры 781 Класс Shape определяет два свойства типа Brush: Stroke и Fill. Свойство Stroke задает кисть, используемую для рисования линий (в том числе контуров эллип- сов, прямоугольников и многоугольников), а свойство Fill задает кисть для за- полнения внутренних областей. По умолчанию оба свойства равны null, и если хотя бы одна из кистей не задана, объект не будет виден на экране. Хотя это и не очевидно до знакомства с полным набором свойств, классы, производные от Shape, поддерживают две парадигмы прорисовки. Классы Line, Path, Polygon и Polyline содержат свойства определения объектов в двумерной сис- теме координат — объекты Point или их аналоги. Например, класс Line содержит свойства XI, Х2, Y1 и Y2 для определения начальной и конечной точек линии. С объектами Ellipse и Rectangle дело обстоит иначе. Они не определяются в дву- мерной системе координат. Для задания конкретных размеров этих графических объектов используются свойства Width и Height, наследуемые от FrameworkElement. Тем не менее задавать эти свойства не обязательно. В одной из начальных глав книги я показал, как создать объект Ellipse и задать его в качестве содержимого окна, чтобы эллипс заполнял всю клиентскую область. Конечно, такой способ рисования графических объектов весьма удобен. Но на практике часто требуется объединить несколько графических объектов в со- ставное изображение — возможно, с перекрытием. В подобных ситуациях по- требуется панель, допускающая перекрытие дочерних элементов. Такие панели, как DockPanel, StackPanel, WrapPanel, Grid и UniformGrid, хорошо подходят для выво- да элементов, отделенных друг от друга, поэтому для общего графического про- граммирования лучше поискать другое решение. Панель Canvas отлично подходит для вывода графики, так как она создавалась именно для этой цели. Размещая объект Ellipse или Rectangle на панели Canvas, необходимо задать свойства Width и Height, или объект будет обладать нулевыми размерами и, как следствие, станет невидимым. (Вообще-то это не совсем так — можно обойтись свойствами MinWidth и MinHeight, и объект с нулевыми размера- ми будет видимым, если его свойство Stroke отлично от null, а свойство Stroke- Thickness больше 1... но вы понимаете, о чем я.) Вероятно, кроме свойств Width и Height, для Ellipse или Rectangle стоит задать присоединенные свойства Canvas.Left и Canvas.Top. Эти свойства задают местопо- ложение левого верхнего угла Ellipse или Rectangle по отношению к левому верх- нему углу Canvas. Вместо них также можно использовать свойства Canvas.Right и Canvas.Bottom, задающие смещение объекта по отношению к правой и нижней стороне Canvas. Для других потомков Shape — Line, Path, Polygon и Polyline — задавать присо- единенные свойства Canvas не обязательно, потому что положение объекта на Canvas определяется его координатами. Для примера возьмем следующий эле- мент Line в коде XAML: <L1ne Xl="100” Yl=”50” X2=”400” Y2=,,100" Stroke=”Blue” /> Начало линии находится в точке (100, 50); это означает, что линия начинается в 100 аппаратно-независимых единицах от левой стороны Canvas и в 50 единицах от верхней стороны. Линия заканчивается в точке с координатами (400,100), так- же заданными по отношению к левому верхнему углу Canvas.
782 Глава 27. Графические фигуры Хотя присоединенные свойства Canvas не обязательны для элемента Line, их можно включить в элемент. Пример: <Line Xl=”100” Yl="50" Y2="100" Stroke="Blue" Canvas.Left="25" Canvas.Top=”150” /> Фактически свойство Canvas.Left прибавляется ко всем координатам X линии, а свойство Canvas.Top — ко всем координатам Y. Линия сдвигается на 25 единиц вправо и 150 единиц вниз. Теперь она начинается в точке (125, 200) и заканчи- вается в точке (425, 250) относительно левого верхнего угла Canvas. Свойства Canvas.Left и Canvas.Top могут быть отрицательными; в этом случае элемент сдви- гается влево и вверх. Canvas — не единственный тип панели, которая допускает произвольное раз- мещение дочерних элементов, необходимое для большинства задач графическо- го программирования. Одна ячейка панели Grid тоже может содержать несколь- ко дочерних объектов, причем в некоторых случаях это даже удобно. Вы можете разместить в ячейке Grid несколько элементов Line, Path, Polygon и Polyline, и их координаты будут определять их размещение относительно левого верхнего угла ячейки. Но если задать свойству HorizontalAlignment этих элементов значение Right, объекты позиционируются относительно правой стороны ячейки. Наконец, со свойством HorizontalAlignment, равным Center, объект позиционируется в центре... до определенной степени. Ширина объекта вычисляется как расстояние от 0 до координаты X крайней правой точки. Например, первый элемент Line из преды- дущего примера будет рассматриваться как имеющий ширину 400, и по центру выравнивается эта общая ширина, поэтому сам объект будет смещен немного вправо от центра. Аналогичная логика используется и для VerticalAlignment. Размещение нескольких объектов Ellipse и Rectangle в ячейке Grid работает не очень хорошо. Для этих элементов можно задать свойства Width и Height, а также свойство Margin, но для их позиционирования придется использовать свойства HorizontalAlignment и VerticalAlignment, но такому способу не хватает универсальности. Давайте поближе познакомимся с классами Polygon и Polyline (знакомство с классом Path откладывается до следующей главы). Эти классы очень похожи: каждый содержит свойство Points типа Pointcollection. Как было показано в гла- ве 20, XAML позволяет задать массив точек с чередованием координат X и Y: Po1nts="100 50 200 50 200 150 100 150" Эти четыре точки образуют квадрат, левый верхний угол которого находится в точке (100, 50), а правый нижний — в точке (200, 150). Конечно, к числу мож- но добавить суффикс рх, более наглядно обозначающий аппаратно-независимые координаты. Суффикс ст соответствует сантиметрам, in — дюймам, a pt — пунк- там (1/72 дюйма). Хотя оба класса, Polygon и Polyline, предназначены для рисования последова- тельностей отрезков, эти классы часто применяются для рисования кривых. Трюк заключается в том, чтобы использовать очень большое количество очень корот- ких линий. Любая математически определяемая кривая может быть нарисована с использованием классов Polygon и Polyline. Пусть вас не смущают сотни и тыся- чи вершин — эти классы создавались для этой цели.
Графические фигуры 783 Конечно, вариант с заданием нескольких тысяч точек в Polyline возможен толь- ко в коде С#. Следующая программа рисует синусоиду с амплитудой положи- тельных и отрицательных значений в один дюйм и периодом в 4 дюйма. Общие горизонтальные размеры кривой составляют 2000 аппаратно-независимых единиц. SineWave.es //---------------------------------------- // SineWave.cs (с) 2006 by Charles Petzold И----------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace SineWave public class SineWave : Window { [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new SineWaveO); } public SineWaveO Title = "Sine Wave"; // Объект Polyline назначается содержимым окна Polyline poly = new PolylineO; poly.VerticalAlignment = VerticalAlignment.Center; poly.Stroke = SystemColors.WindowTextBrush; poly.StrokeThickness = 2; Content = poly; // Определение точек for (int i = 0: i < 2000; i++) poly.Points.Add( new Point(i, 96 * (1 - Math.SinO * Math.PI / 192)))); } } } Обратите внимание: объект Polyline задается свойству Content объекта Window, а вертикальные координаты синусоиды масштабируются в диапазоне от 0 до 192. Следовательно, свойство VerticalAlignment объекта Polyline можно задать равным Center, чтобы синусоида выравнивалась по центру окна. Если вертикальные ко- ординаты будут изменяться в диапазоне от -96 до 96 (что решается удалением 1
784 Глава 27. Графические фигуры и знака минуса перед Math.Sin), кривая рассматривается как имеющая вертикаль- ный размер 96 вместо 192. Фактически отрицательные координаты игнориру- ются при вычислении высоты элемента для целей выравнивания. В результате кривая будет размещаться выше центра окна. В программе SineWave продемонстрирован один из способов заполнения свойства Points объекта Polyline. Подозреваю, что его эффективность оставляет же- лать лучшего: при добавлении новых точек объекту Pointcollection придется вы- делять дополнительную память. Если вы собираетесь заполнять свойство Points подобным образом, количество точек желательно задать до начала цикла: poly.Points = new PolntCol1ectlon(2000): Более эффективный подход к определению свойства Points продемонстриро- ван в программе Spiral, использующей параметрические уравнения для рисова- ния спирали. Созданный в этой программе массив объектов Point становится ар- гументом конструктора Pointcollection. Spiral.cs //--------------------------------------- // Spiral.cs (с) 2006 by Charles Petzold //--------------------------------------- using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: using System.Windows.Shapes: namespace Spiral { public class Spiral : Window { const int revs = 20; const int numpts = 1000 * revs; Polyline poly: [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new SpiralO); ) public SpiralO { Title = "Spiral”: // Объект Canvas назначается содержимым окна Canvas canv = new CanvasO; canv.SizeChanged += CanvasOnSizeChanged;
Графические фигуры 785 Content = canv; // Назначение Polyline дочерним объектом Canvas poly = new PolylineO; poly.Stroke = SystemColors.WindowTextBrush; canv.Chi 1dren.Add(poly); // Определение точек Point[] pts = new Point[numpts]; for (int i = 0; i < numpts; i++) { double angle = i * 2 * Math.PI / (numpts / revs); double scale = 250 * (1 - (double) i / numpts); pts[i].X = scale * Math.Cos(angle); pts[i].Y = scale * Math.Sin(angle); } poly.Points = new PointCollection(pts); ) void CanvasOnSizeChanged(object sender, SizeChangedEventArgs args) { Canvas.SetLeft(poly, args.NewSize.Width / 2); Canvas.SetTop(poly, args.NewSize.Height / 2); } } В программе Spiral также продемонстрирован другой способ центровки объ- екта в клиентской области. Спираль рисуется на панели Canvas, но выравнивает- ся по центру относительно точки (0, 0). При каждом изменении размера Canvas обработчик события CanvasOnSizeChanged переводит присоединенные свойства Left и Тор объекта Polyline в центр Canvas, и спираль позиционируется заново. Класс Polyline предназначен для рисования серий соединенных отрезков, а класс Polygon определяет замкнутую область для закраски (и возможно, обводки кон- туром). Тем не менее эти два класса имеют больше общего, чем может пока- заться. Следующий элемент Polygon включает две кисти для рисования контура и заполнения объекта: <Polygon Points="100 50. 200 50, 200 150, 100 150” Stroke="Red" Fill=”Blue" /> Обратите внимание: в коллекции Points отсутствует «явное» замыкание. При желании можно включить завершающую точку (100, 50), но это не обязательно. Класс Polygon автоматически строит отрезок от конечной точки до начальной, замыкая квадрат и определяя замкнутую область. Линии, определяющие квад- рат (включая последнюю добавленную линию), окрашиваются кистью Stroke, а квадрат заполняется кистью Fill. Хотя в документации Polyline сказано, что «задание свойства Fill для Polyline не имеет никаких последствий», это не так. Если свойство Fill отлично от null, Polyline
786 Глава 27. Графические фигуры заполняет ту же область, что и Polygon. Единственное реальное отличие заклю- чается в том, что Polyline не добавляет автоматически последнюю линию, хотя и заполняет внутреннюю область, как если бы эта линия присутствовала. Следующий XAML-файл рисует мой портрет, составленный из объектов Ellipse, Polygon и Line. SelfPortraitSansGlasses.xaml <।_ _ ========================================================== SelfPortraitSansGlasses.xaml (c) 2006 by Charles Petzold Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <!-- Голова --> <E1lipse Canvas.Left="96" Canvas.Top="96" Width="144" Height="240" Fill="PeachPuff" Stroke="Black" /> <!-- Уши --> Polygon Points="100 192, 84 168. 84 240. 100 216" Fi11="SandyBrown" Stroke="Black" /> <Polygon Points="236 192. 252 168, 252 240, 236 216" Fill="SandyBrown" Stroke="Black" /> <!-- Глаза --> <E1lipse Canvas.Left=”120" Canvas.Top="168" Width="36" Height="36" Fill="White" Stroke="Black" /> <E1lipse Canvas.Left="180" Canvas.Top="168" Width=”36" Height="36" Fill="White" Stroke="Black" /> <!-- Радужная оболочка --> <E1lipse Canvas.Left="129" Canvas.Top="177" Width="18" Height="18" Fill="Brown" Stroke="Black" /> <E1lipse Canvas.Left="189" Canvas.Top="177" Width="18" Height="18" Fill="Brown" Stroke="Black" /> <!-- Hoc --> <Polygon Points="168 192, 158 240, 178 240" Fill="Pink" Stroke="Black" /> <!-- Pot --> <E1lipse Canvas.Left="120" Canvas.Top="260" Width="96" Height="24" Fill="White" Stroke="Red" StrokeThickness="8" />
Графические фигуры 787 <!-- Борода --> <L1ne Xl="120" Yl="288" Х2="120” Y2="336” Stroke=”Black" StrokeThickness="2" /> <Line Xl="126" Yl^’290" X2=”126” Y2=”338" Stroke=”Black” StrokeThickness="2" /> <Line Xl="132" Yl=,,292” ХЗ^ЗЗ" Y2=”340” Stroke=”Black” StrokeThickness="2" /> <Line Xl="138" Yl="294" X2="138" Y2=”342” Stroke=”Black" StrokeThickness="2" /> <Line Xl="144" Yl=”296" X2=,,144” Y2=”344” Stroke=”Black” StrokeThickness="2" /> <Line Xl="150" Yl="297” X2=,,150” Y2=”345” Stroke=”Black” StrokeThickness="2" /> <Line Xl="156" Yl="298" X2=”156" Y2=,,346” Stroke=”Black" StrokeTh1ckness="2" /> <Line Xl="162" Yl="299" X2=,,162” Y2=”347” Stroke=”Black” StrokeThickness="2" /> <Line Xl="168" Yl="300" Х2=,,168" Y2=,,348” Stroke=”Black” StrokeThickness="2" /> <Line Xl="174" Yl="299” X2=,,174” Y2=”347” Stroke=”Black” StrokeThickness="2" /> <L1ne Xl="180" Yl="298" X2=”180” Y2="346” Stroke=”Black” StrokeThickness="2" /> <Line Xl="186" Yl="297" X2="186” Y2=”345” Stroke=,,Black” StrokeThickness="2" /> <Line Xl=”192” Yl=”296” X2="192" Y2=”344” Stroke=”Black” StrokeThickness="2" /> <Line Xl="198” Yl=,,294n X2="198” Y2=”342” Stroke="Black” StrokeThickness="2" /> <Line Xl="204" Yl=”292” X2=”204” Y2=”340” Stroke=”Black" StrokeThickness="2" /> <Line Xl=”210” Yl="290” ХЗ^ЗЮ" Y2=”338” Stroke=”B1ack” StrokeThickness="2" /> <Line Xl^lG” Yl="288" X2=”216” УЗ^’ЗЗб" Stroke=’’Black” StrokeThickness="2" /> </Canvas> Поскольку класс Shape является производным от FrameworkElement, для опре- деления основных свойств объектов можно использовать элементы Style. Напри- мер, чтобы избавиться от многократных присваиваний атрибутов Stroke и Stroke- Thickness в элементах Line, достаточно включить определение Style следующего вида: <Style TargetType="{x:Type Line}"> <Setter Property="Stroke" Value="Black" /> <Setter Property="StrokeTh1ckness" Value="2" /> </Style> К сожалению, стили не позволят сократить общую длину этого конкретного файла, но я буду использовать их во многих программах этой главы.
788 Глава 27. Графические фигуры Изображение, нарисованное файлом SelfPortraitSansGlasses.xaml, имеет фикси- рованный размер. Иногда размер графического изображения требуется увели- чить или уменьшить в зависимости от размера окна программы. Объект Viewbox автоматически изменяет размеры своего содержимого по своим размерам. Сле- дующий, относительно простой пример показывает, как это делается. ScalableFace.xaml <।_ _ ========================================= ScalableFace.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas .nricrosoft.com/winfx/2006/xanil/presentation" xml ns:x=”http://schemas.microsoft.com/wi nfx/2006/xaml" Background="White"> <Viewbox> Canvas Width="100" Height="100"> <E11 ipse Canvas.Left=”5" Canvas.Top="5" Width="90" Height="90" Stroke="Black" /> <!-- Глаза --> <E11ipse Canvas.Left="25" Canvas.Top="30" Width="10" Height="10" Stroke="Black" /> <E11 ipse Canvas.Right="25" Canvas.Top="30" Width="10" Height="10" Stroke="Black" /> <!-- Брови --> <Polyline Points="25 25. 30 20, 35 25" Stroke="Black" /> <Polyline Points="65 25. 70 20. 75 25" Stroke="Black" /> <!-- Hoc --> <Polyline Points="50 40. 45 60. 55 60. 50 40" Stroke="Black" /> <!-- Pot --> <Polyline Points="25 70 50 80 75 70" Stroke="Black" /> </Canvas> </Viewbox> </Page> По умолчанию Viewbox одинаково масштабирует изображение по вертикали и горизонтали, чтобы исключить возможные искажения. Если вы предпочитаете, чтобы изображение занимало как можно большую часть выделенного простран- ства (даже при некотором искажении), задайте свойству Stretch значение Fill. Иногда линии, образующие объект Polyline или Polygon, пересекаются, и тогда возникает проблема: какие из замкнутых областей следует заполнять, а какие — нет? В обоих классах определяется свойство FillRule, определяющее способ выбора заполняемых областей. Перечисляемый тип его допустимых значений включает элементы FillRule.EvenOdd (используется по умолчанию) и FillRule.NonZero. Разли- чия между этими двумя способами часто объясняются на примере пятиконеч- ной звезды, как это сделано в следующем XAML-файле.
Графические фигуры 789 Чтобы избежать частых повторений, файл TwoStars.xaml начинается с опре- деления стиля многоугольников. В элементах Setter задаются четыре свойства Polygon: Points, Fill, Stroke и StrokeThickness. Звезда определяется в системе коорди- нат с центром в точке (0, 0) и радиусом в один дюйм. TwoStars.xaml <! -- =========================================== TwoStars.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" TextBlock.FontSi ze="16"> Canvas. Resources> <!-- Определение свойств, общих для обеих фигур --> <Style x:Key="star"> <Setter Property="Polygon.Points" Value="0 -96. 56. 78, -91 -30. 91 -30. -56 78" /> <Setter Property="Polygon.Fill" Value="Blue" /> <Setter Property="Polygon.Stroke" Value="Red" /> <Setter Property="Polygon.StrokeThickness" Value="3" /> </Style> </Canvas.Resources> <!-- Рисование первой фигуры в режиме "EvenOdd" --> <TextBlock Canvas.Left="48" Canvas.Top="24" Text="FillRule = EvenOdd" /> <Polygon Style="{StaticResource star}" FillRule="EvenOdd" Canvas.Left="120" Canvas.Top="168" /> <!-- Рисование второй фигуры в режиме "NonZero" --> <TextBlock Canvas.Left="288" Canvas.Top="24" Text="FillRule = NonZero" /> <Polygon Style="{StaticResource star}" FillRule="NonZero" Canvas.Left="360" Canvas.Top="168" /> </Canvas> Файл отображает два элемента TextBlock со значениями FillRule, а также две пятиконечные звезды, нарисованные с использованием элементов Polygon. У пер- вой звезды свойству FillRule задается значение "EvenOdd", а у второй — "NonZero". Звезды смещаются от позиции, определяемой коллекцией Points, при помощи присоединенных свойств Canvas.Left и Canvas.Top.
790 Глава 27. Графические фигуры Как работает режим по умолчанию EvenOdd? Представьте линию, проведен- ную от точки внутри замкнутой области в бесконечность. Замкнутая область заполняется только в том случае, если эта воображаемая линия пересекает не- четное количество границ. Именно по этой причине лучи первой звезды запол- нены, а центральная область — нет. В том, что этот алгоритм действительно работает, нам поможет убедиться файл EvenOddDemo.xaml. Он очень похож на TwoStars.xaml, но нарисованная фи- гура имеет более сложную структуру, а количество границ, отделяющих замкну- тые области от бесконечности, достигает шести. EvenOddDemo.xaml <।_ _ ============================================== EvenOddDemo.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" TextBlock.FontSi ze="16"> <Canvas.Resources> <!-- Определение свойств, общих для обеих фигур --> <Style x:Key="figure"> <Setter Property="Polygon.Points" Value=" 0 0, 0 144, 144 144, 144 24. 24 24. 24 168. 168 168. 168 48. 48 48. 48 192. 192 192. 192 72. 72 72. 72 216. 216 216. 216 96. 96 96. 96 240, 240 240, 240 120. 120 120. 120 264. 264 264. 264. 0" /> <Setter Property="Polygon.Fill" Value="Blue" /> <Setter Property="Polygon.Stroke" Value="Red" /> <Setter Property="Polygon.StrokeThickness" Value="3" /> </Sty1e> </Canvas.Resources> <!-- Рисование первой фигуры в режиме "EvenOdd" --> <TextBlock Canvas.Left="48" Canvas.Top="24" Text="FillRule = EvenOdd" /> <Polygon Style="{StaticResource figure}" FillRule="EvenOdd" Canvas.Left="48" Canvas.Top="72" /> <!-- Рисование второй фигуры в режиме "NonZero" --> <TextBlock Canvas.Left="360" Canvas.Top="24"
Графические фигуры 791 Text="F111Rulе = NonZero" /> <Polygon Style="{StaticResource figure}" FillRule="NonZero" Canvas.Left="360" Canvas.Top=,72" /> </Canvas> Возможно, вы уже решили, что режим NonZero просто заполняет все замкну- тые области? Иногда результат оказывается именно таким, но алгоритм более сложен. Многоугольник определяется серией соединенных точек (ptl, pt2, pt3, pt4 и т. д.), причем эти точки соединяются в определенном направлении: от ptl к pt2, от pt2 к pt3 и т. д. Чтобы определить, должна ли замкнутая область заполняться в режиме Non- Zero, снова представьте линию, проведенную от точки в бесконечность. Если воображаемая линия пересекает нечетное количество границ, эта область за- полняется, как и по правилу EvenOdd. Но если воображаемая линия пересекает четное количество границ, область заполняется только в том случае, если ко- личество границ, идущих в одном направлении (по отношению к воображаемой линии), не равно количеству границ, идущих в другом направлении. Другими словами, область заполняется, если разность между количеством границ, иду- щих в двух направлениях, отлична от нуля, на что и указывает название пра- вила NonZero. Направления рисования линий показаны на следующем рисунке. С правилами EvenOdd и NonZero три I-образные области с номерами от 1 до 3 всегда заполняются. Две внутренние области 4 и 5 не заполняются в режиме EvenOdd, потому что от бесконечности их отделяет четное количество границ. Но в режиме NonZero область 5 будет заполнена, потому что для выхода из цен- тра фигуры за ее пределы необходимо пересечь две линии, идущие в одном на- правлении. С другой стороны, область 4 заполняться не будет: здесь снова пере- секаются две линии, но эти две линии идут в разных направлениях.
792 Глава 27. Графические фигуру Следующий XAML-файл рисует две разновидности этой фигуры. TwoFigures.xaml <I_ _ ============================================= TwoFigures.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" TextBlock.FontSi ze="16"> <Canvas.Resources> <!-- Определение свойств, общих для обеих фигур --> <Style x:Key="figure"> <Setter Property="Polygon.Points" Value="0 48, 0 144, 96 144, 96 0, 192 0, 192 96. 48 96, 48 192, 144 192 144 48" /> <Setter Property="Polygon.Fill" Value="Blue" /> <Setter Property="Polygon.Stroke" Value="Red" /> <Setter Property="Polygon.StrokeThickness" Value="3" /> </Style> </Canvas.Resources> <!-- Рисование первой фигуры в режиме "EvenOdd" --> <TextBlock Canvas.Left="48" Canvas.Top="24" Text="FillRule = EvenOdd" /> <Polygon Style="{StaticResource figure}" FillRule="EvenOdd" Canvas.Left="48" Canvas.Top="72" /> <!-- Рисование второй фигуры в режиме "NonZero" --> <TextBlock Canvas.Left="288" Canvas.Top="24" Text="FillRule = NonZero" /> <Polygon Style="{StaticResource figure}" FillRule="NonZero" Canvas.Left="288" Canvas.Top="72" /> </Canvas> Для определения внешнего вида прямоугольников, эллипсов и многоугольни- ков до настоящего момента использовались два свойства Shape: свойство Stroke задавало кисть для рисования контуров, а свойство StrokeThickness — толщину линий. Пространство имен System.Windows.Media содержит класс Реп, который опреде- ляет восемь свойств, связанных с рисованием линий. Хотя класс Реп используется в графических операциях WPF, в классе Shape он не определяется. Очевидно, он
Графические фигуры 793 используется во внутренней реализации, но класс Shape определяет собственные свойства (начинающиеся со слова Stroke), аналогичные свойствам Реп. В следую- щей таблице представлена сводка свойств Реп и соответствующих свойств Shape. ^Свойство Реп Тип Свойство Shape Brush Brush Stroke Thickness Double StrokeThickness StartLineCap PenLineCap StrokeStartLineCap EndLineCap PenLineCap StrokeEndLineCap LineJoin PenLineJoin StrokeLineJoin MiterLimit Double StrokeMiterLimit DashStyle DashStyle StrokeDashArray (DoubleCollection) StrokeDashOffset (double) DashCap PenLineCap Stroke DashCap Определение этих свойство в классе Shape упрощает «ручное» написание кода XAML. При определении свойства Stroke в классе Shape цвет линии задает- ся следующим образом: <Ellipse Stroke="Blue" ... /> Если бы в классе Shape определялось свойство Реп типа Реп, то же самое при- шлось бы делать так: <Е111pse ... > <Е111pse.Реп> <Pen Brush="Blue" .../> </Е111pse.Реп> </Е111pse> Свойство StrokeThickness, определяемое классом Shape (аналог свойства Thickness класса Реп), задает толщину линии в аппаратно-независимых единицах. С уве- личением толщины линии становятся хорошо видны концы отрезков. По умол- чанию толстая линия (показанная серым на рисунке) «накрывает» геометриче- скую линию, определяющую положение начальной и конечной точек (на рисунке она выделена черным), и резко прерывается в этих точках. Завершители (то есть оформление начала и конца линии) задаются значения- ми из перечисления PenLineCap. По умолчанию используется значение PenLine- Cap.Flat. При других значениях — Square, Round и Triangle — отрезок фактически продлевается за его геометрическую начальную и конечную точку.
794 Глава 27. Графические фигуры Следующий автономный XAML-файл демонстрирует все четыре разновидно- сти завершителей. Для наглядности я наложил на рисунок тонкую черную ли- нию, обозначающую ее геометрическое начало и конец. LineCaps.xaml <।_ _ =========================================== LineCaps.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml" Orientation="Horizontal"> <StackPanel.Resources> <Style TargetType="{x:Type Canvas}"> <Setter Property="Width" Value="150" /> <Setter Property="Margin" Value="12" /> </Sty1e> <Style x:Key="thin"> <Setter Property="Line.xr Value="00" /> <Setter Property="Line.Yl" Value="50" /> <Setter Property="Line.X2" Value="100" /> <Setter Property="Line.Y2" Value="50" /> <Setter Property="Line.Stroke" Value="Black" /> </Style> <Style x:Key="thick" BasedOn="{StaticResource thin}"> <Setter Property="Line.Stroke" Value="LightGray" /> <Setter Property="Line.StrokeThickness" Value="25" /> </Style> </StackPanel.Resources> < ! -- PenLineCap.Flat. --> <Canvas> <TextBlock Text="PenLineCap.Flat" /> <Line Style="{StaticResource thick}" StrokeStartLineCap="Flat" StrokeEndLineCap="Flat" /> <Line Style="{StaticResource thin}" /> </Canvas> < !-- PenLineCap.Square. --> <Canvas> <TextBlock Text="PenLineCap.Square" /> <Line Style="{StaticResource thick}" St rokeSta rtL i neCa p="Sq ua re" StrokeEndLineCap="Square" /> <Line Style="{StaticResource thin}" /> </Canvas>
Графические фигуры 795 < !-- PenLineCap.Round. --> <Canvas> <TextBlock Text="PenLineCap.Round" /> <Line Style="{StaticResource thick}" StrokeStartLineCap="Round" StrokeEndLineCap="Round" /> <Line Style="{StaticResource thin}" /> </Canvas> < !-- PenLineCap.Triangle. --> <Canvas> <TextBlock Text="PenLineCap.Triangle" /> <Line Style="{StaticResource thick}" StrokeStartLineCap="Triangle" StrokeEndLineCap="Triangle" /> <Line Style="{StaticResource thin}" /> </Canvas> </StackPanel> Аналогичная проблема возникает для ломаных в местах присоединения от- резков. Свойству StrokeLineJoin объекта Shape задается значение из перечисления PenLineJoin: Bevel, Miter пли Round. При соединениях типа Bevel и Miter отрезки об- разуют четко прочерченную границу. Соединение Bevel отсекает угол, а соедине- ние Miter — нет (по крайней мере, до определенной степени). Свойство StrokeLineJoin отражается на внешнем виде объектов Rectangle, Polyline и Polygon. В этом нам поможет убедиться следующая автономная XAML-про- грамма. LineJoins.xaml <1 __ ============================================ LineJoins.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Orientation="Horizontal"> <StackPanel.Resources> <Style TargetType="{x:Type TextBlock}"> <Setter Property="Canvas.Left" Value="25" /> </Style> <Style TargetType="{x:Type Canvas}"> <Setter Property="Width" Value="150" /> <Setter Property="Margin" Value="12" /> </Sty1e> <Style TargetType="{x:Type Rectangle}"> <Setter Property="Width" Value="100" />
796 Глава 27. Графические фигуры <Setter Property="Height" Value="100" /> <Setter Property="Canvas.Top" Value="50" /> <Setter Property="Canvas.Left" Value="25" /> <Setter Property="Stroke" Value="Black" /> <Setter Property="StrokeThickness" Value="25" /> </Style> </StackPanel.Resources> < !-- PenLIneJoin.Bevel. --> <Canvas> <TextBlock Text="PenLineJoin.Bevel" /> <Rectangle StrokeLineJoin="Bevel" /> </Canvas> < !-- PenLineJoin.Round. --> <Canvas> <TextBlock Text="PenLineJoin.Round" /> <Rectangle StrokeLineJoin="Round" /> </Canvas> < !-- PenLineJoin.Miter. --> <Canvas> <TextBlock Text="PenLineJoin.Miter" /> <Rectangle StrokeLineJoin="Miter" /> </Canvas> </StackPanel> У соединений Miter существует специфическая проблема, которая проявляется только при соединении отрезков под очень острым углом. Файл PenProperties.xaml поможет нам разобраться в ее сути. PenProperties.xaml <।_ _ ================================================ PenProperties.xaml (с) 2006 by Charles Petzold <Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <!-- Рисование ломаной на панели Grid с одной ячейкой --> <Polyline Margin="0.5in, 1.5in, 0. 0" Points="0 0. 500 25. 0 50" VerticalAlignment="Center" Stroke="Blue" StrokeThickness="{Binding ElementName=sliderThickness. Path=Value }" StrokeStartLineCap="{Binding ElementName=lstboxStartLineCap. Path=Selectedltem.Content}" StrokeEndLineCap="{Binding ElementName=lstboxEndLineCap.
Графические фигуры 797 Раth=Se1ectedItem.Content}" StrokedneJoin="{Binding ElementName=lstboxLineJoin, Path=Selectedltem.Content}" StrokeMiterLimit="{Binding ElementName=sliderMiterLimit, Path=Value }" /> <!-- Создание горизонтальной панели StackPanel в той же ячейке Grid --> <StackPanel Grid.Column="0" Margined, 12, 0, 0“ Orientation^’Horizontal" > <!-- Определение стиля для пяти "интерфейсных" групп --> <StackPanel.Resources> <Style x:Key="uigroup"> <Setter Property="StackPanel.VerticalAlignment" Value="Top" /> <Setter Property="StackPanel.Width" Value="100" /> <Setter Property="StackPanel.Margin" Value="12, 0. 12. 0" /> </Style> </StackPanel.Resources> <!-- Элемент Slider для изменения свойства StrokeThickness --> <StackPanel Style="{StaticResource uigroup}"> <Label Content="_Thickness" /> <S1ider Name="sliderThickness" Minimum="0" Maximum="100" Value="24" /> </StackPanel> <!-- Элемент ListBox для изменения свойства StrokeStartLineCap --> <StackPanel Style="{StaticResource uigroup}"> <Label Content="_StartLineCap" /> <ListBox Name="1stboxStartLineCap"> <ListBoxItem Content="{x:Static PenLineCap.Flat}" /> <ListBoxItem Content="{x:Static PenLineCap.Square}" /> <ListBoxItem Content="{x:Static PenLineCap.Round}" /> <ListBoxItem Content="{x:Static PenLineCap.Triangle}" /> </ListBox> </StackPanel> <!-- Элемент ListBox для изменения свойства StrokeEndLineCap --> <StackPanel Style="{StaticResource uigroup}"> <Label Content="_EndLineCap" /> <Li stBox Name="1stboxEndLineCap"> <ListBoxItem Content="{x:Static PenLineCap.Flat}" /> <ListBoxItem Content="{x:Static PenLineCap.Square}" />
798 Глава 27. Графические фигуры <L1stBoxItem Content="{x:Static PenLineCap.Round}" /> <ListBoxItem Content="{x:Static PenLineCap.Triangle}" /> </ListBox> </StackPanel> <!-- Элемент ListBox для изменения свойства StrokeLineJoin --> <StackPanel Style="{StaticResource uigroup}"> <Label Content="_LineJoin" /> <ListBox Name="1stboxLineJoin"> <ListBoxItem Content="{x:Static PenLineJoin.Bevel}" /> <ListBoxItem Content="{x:Static PenLineJoin.Round}" /> <ListBoxItem Content="{x:Static PenLineJoin.Miter}" /> </ListBox> </StackPanel> <!-- Элемент Slider для изменения свойства StrokeMiterLimit --> <StackPanel Style="{StaticResource uigroup}"> <Label Content="_MiterLimit" /> <S1ider Name="s1i derMi terLi mi t" Minimum="0" Maximum="100" Value="10" /> </StackPanel> </StackPanel> </Grid> Программа рисует простую ломаную из двух сегментов, похожую на вытяну- тый знак «>». Кроме того, она создает элементы Slider и ListBox для изменения пяти свойств Polyline. Объект Polyline демонстрирует проблему, возникающую при задании свойст- ву StrokeEndJoin значения LineEndJoin.Miter. Например, у отрезков ломаной толщи- ной 1 дюйм, сходящихся под углом 1°, соединение вытянется более чем на метр! По этой причине свойство StrokeMiterLimit ограничивает длину соединения посред- ством отсечения заострения. Чтобы увидеть действие свойства StrokeMiterLimit, выберите соединение Miter и увеличьте толщину линии — после достижения не- которой пороговой величины заостренная вершина отсекается. Два последних свойства Реп из приведенной ранее таблицы — DashStyle и Dash- Cap — предназначены для рисования линий, состоящих из пунктира и/или то- чек. Свойство DashStyle содержит объект типа DashStyle — класса, обладающего двумя свойствами: Dashes (объект типа DoubleCollection) и Offset (Double). Элемен- ты коллекции Dashes задают шаблон рисования линии. Класс Shape заменяет объект DashStyle двумя свойствами: StrokeDashArray и Stro- keDashOffset. В следующем XAML-файле свойству StrokeDashArray элемента Line задается значение "12 2 1". OneTwoTwoOne.xaml <I_ _ =============================================== OneTwoTwoOne.xaml (с) 2006 by Charles Petzold
Графические фигуры 799 <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Line Xl="48" Yl="48" X2="1000" Y2="48" Stroke="{DynamicResource {x:Static Systemcolors.WindowTextBrushKey}}" StrokeThickness="12" StrokeDashArray="l 2 2 1" /> </Canvas> При выполнении этого XAML-файла видно, что линия начинается со штри- ха, длина которого совпадает с толщиной линии; далее следует пробел вдвое длиннее толщины линии, затем штрих двойной толщины и пробел одинарной толщины. Далее шаблон повторяется до бесконечности. Значение StrokeDashOffset задает смещение для применения шаблона. Например, в файл OneTwoTwoOne.xaml можно включить атрибут, задающий StrokeDashOffset равным 1: StrokeDashOffset='T' Теперь линия начинается с первого пробела двойной толщины. Впрочем, с массивом StrokeDashArray в файле OneTwoTwoOne.xaml могут возник- нуть проблемы. Класс Shape определяет свойство StrokeDashCap (аналог свойства DashCap класса Реп) типа PenLineCap — того же перечисления, которое использу- ется со свойствами StrokeStartLineCap и StrokeEndLineCap. Напомню, что перечис- ление состоит из четырех значений: Flat, Square, Round и Triangle. По умолчанию используется значение Flat, при котором длина штрихов и пробелов точно со- ответствует маске StrokeDashArray. Однако другие виды завершителей факти- чески увеличивают длину штрихов. Попробуйте включить следующий атрибут в XAML-файл: StrokeDashCap="Roiind" Точки перестали быть точками — теперь они больше похожи на сосиски. Ве- роятно, для получения результата, близкого к предыдущему шаблону, вам при- дется заменить StrokeDashArray на "О 3 1 2”. Следующий автономный XAML-файл выводит четыре варианта завершения с использованием шаблона StrokeDashArray "2 2”. DashCaps. xaml <।_ _ =========================================== DashCaps.xaml (c) 2006 by Charles Petzold <Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Grid.Resources> <Style TargetType="{x:Type TextBlock}"> <Setter Property="FontSize" Value="16" /> <Setter Property="Margin" Value=,,24" />
800 Глава 27. Графические фигуры <Setter Property="VerticalAlignment" Value="Center" /> </Style> <Style TargetType="{x:Type Line}"> <Setter Property="Grid.Column" Value='T' /> <Setter Property="Yl" Value="30" /> <Setter Property="X2" Value="400" /> <Setter Property="Y2" Value="30" /> <Setter Property="StrokeThickness" Value="25" /> <Setter Property="Stroke" Value="Black" /> <Setter Property="StrokeDashArray" Value="2 2" /> <Setter Property="StrokeStartLineCap" Value="{Binding RelativeSource={RelativeSource self}, Path=StrokeDashCap}" /> <Setter Property="StrokeEndLineCap" Value="{Binding RelativeSource={RelativeSource self}, Path=StrokeDashCap}" /> </Style> </Grid.Resources> <Gr1d.RowDefi ni ti ons> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </G г id.RowDefi ni ti ons> <Gri d.ColumnDefi ni ti ons> ColumnDefinition Width="Auto" /> ColumnDefinition Width="Auto" /> </G r i d.ColumnDefi ni ti ons> <!-- PenLineCap.Fl at. --> <TextBlock Grid.Row="0" Text="PenLineCap.Flat" /> Cine Grid.Row="0" /> <!-- PenLineCap.Square. --> <TextBlock Grid.Row="l" Text="PenLineCap.Square" /> Cine Grid.Row=”l" StrokeDashCap="Square" /> <!-- PenLineCap.Round. --> <TextBlock Grid.Row="2" Text="PenLineCap.Round" /> Cine Grid.Row="2" StrokeDashCap="Round" /> <!-- Triangle.Triangle. --> <TextBlock Grid.Row="3" Text="PenLineCap.Triangle" /> Cine Grid.Row="3" StrokeDashCap="Triangle" /> </Grid>
Графические фигуры 801 Определение стиля для Line включает привязку от свойства StrokeDashCap сра- зу к двум свойствам, StrokeStartLineCap и StrokeEndLineCap. Линии выглядят наи- более «естественно», когда этим трем свойствам задано одно значение. Только стиль Flat нормально чередуется между 2-единичным штрихом и 2-еди- ничным пробелом. Другие варианты завершителей выглядят таким образом, ко- гда StrokeDashArray задается маска "1 3". Пространство имен System.Windows.Media включает как класс DashStyle (со свой- ствами Dashes и Offset), так и класс DashStyles (обратите внимание на множест- венное число). Класс DashStyles содержит пять статических свойств с именами Solid, Dot, Dash, DashDot и DashDotDot типа DashStyle. В следующей таблице приве- дены свойства Dashes для каждого из статических свойств DashStyles. Статическое свойство DashStyles Свойство Dashes Solid Dot 02 Dash 22 DashDot 2 2 0 2 DashDotDot 220202 В классе Реп свойство DashCap по умолчанию равно PenLineCap.Square; это оз- начает, что точки и штрихи по умолчанию расширяются на половину толщины линии. Различные массивы Dashes выбирались с учетом этого обстоятельства. Но эти значения не подходят, когда свойство DashCap равно PenLineCap.Flat (зна- чение по умолчанию для свойства StrokeDashCap, определяемого классом Shape). Если задать атрибут StrokeDashArray="0 2" со значением Flat, вы вообще не увидите линию, потому что штрихи будут иметь нулевую длину. Я рекомендую вручную задавать свойство StrokeDashArray в за- висимости от выбранного свойства StrokeDashCap. В этой главе были описаны все классы, производные от Shape, кроме класса траекторий Path. Графическая траектория представляет собой совокупность от- резков и кривых. К свойствам, определяемым классом Shape, класс Path добавля- ет единственное свойство Data типа Geometry, предоставляющее в распоряже- ние разработчика множество нетривиальных графических возможностей. Классы Geometry и Path рассматриваются в следующей главе.
Q Классы Geometry /О и Path Мы рассмотрели многих потомков класса Shape: Rectangle, Ellipse, Line, Polyline и Polygon. Осталось рассмотреть только класс Path — бесспорно, самый мощный из всех графических классов, который включает функциональность всех осталь- ных потомков Shape, а также делает много другое. Теоретически для любых опе- раций векторной графики достаточного одного класса Path. Единственный реаль- ный недостаток Path — некоторая пространность записи по сравнению с другими классами Shape. Впрочем, ближе к концу главы я покажу сокращенную запись, которая позволяет получить весьма компактный код. Класс Path определяет всего одно дополнительное свойство Data, которому задается объект типа Geometry. Сам класс Geometry является абстрактным, но как видно из следующей иерархии, он имеет семь производных классов: Object Dispatcherobject (абстрактный) DependencyObject Freezable (абстрактный) Animatable (абстрактный) Geometry (абстрактный) LineGeometry Rectang1 eGeometry El lipseGeometry GeometryGroup CombinedGeometry PathGeometry StreamGeometry Потомки Geometry перечислены в том порядке, в котором они будут рассмат- риваться в этой главе. Эти классы ближе всего подходят к инкапсуляции чис- той аналитической геометрии в графике WPF. Объект Geometry определяется в виде последовательности точек и длин отрезков. Он не умеет прорисовывать себя, поэтому для вывода геометрического объекта с нужными свойствами кис- ти-заполнителя и пера необходимо использовать другой класс (чаще всего Path). Разметка выглядит примерно так: <Path Stroke="Blue" StrokeThickness="3" Fill="Red"> <Path.Data>
Классы Geometry и Path 803 <Е111pseGeometry ... /> </Path.Data> </Path> Класс LineGeometry определяет два свойства: StartPoint и EndPoint. Следующий автономный XAML-файл использует классы LineGeometry и Path для рисования двух пересекающихся линий разных цветов. UneGeometryDemo.xaml < ! - - =================================================== LineGeometryDemo.xaml (с) 2006 by Charles Petzold < Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml "> <Path Stroke="Blue" StrokeThickness="3"> <Path.Data> <LineGeometry StartPoint="96 96" EndPoint="192 192" /> </Path.Data> </Path> <Path Stroke="Red" StrokeThickness="3"> <Path.Data> <LineGeometry StartPoint="192 96" EndPoint="96 192" /> </Path.Data> </Path> </Canvas> На панели Canvas можно разместить несколько элементов Path, причем каж- дый элемент обладает своими атрибутами Stroke и StrokeThickness. Класс Rectangle- Geometry определяет свойство Rect (конечно, с типом Rect), задающее местополо- жение и размер прямоугольника, а также свойства RadiusX и RadiusY, задающие степень закругления углов. В XAML свойство типа Rect задается в виде строки из четырех чисел: координата X левого верхнего угла, координата Y левого верх- него угла, ширина и высота. RectangleGeometryDemo.xaml < I -- ======================================================== RectangleGeometryDemo.xaml (с) 2006 by Charles Petzold < Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Path Fill="Blue" Stroke="Red" StrokeThickness="3"> <Path.Data> <RectangleGeometry Rect="96 48 288 192" RadiusX="24" RadiusY="24" /> </Path.Data> </Path> </Canvas>
804 Глава 28. Классы Geometry и Path Свойства StartPoint и EndPoint, определяемые LineGeometry (а также свойства Rect, RadiusX и RadiusY, определяемые RectangleGeometry), поддерживаются зави- симыми свойствами; это означает, что эти свойства могут использоваться в ани- мации (см. главу 30) и быть приемниками в привязках данных. Класс EllipseGeometry содержит конструктор для построения эллипса на базе объекта Rect. Тем не менее при определении EllipseGeometry в XAML такая воз- можность недоступна. Вместо этого центр эллипса задается свойством Center типа Point, а размеры — свойствами RadiusX и RadiusY типа double. Все три свойства под- держиваются зависимыми свойствами. EllipseGeometryDemo.xaml <!_ _ ====================================================== EllipseGeometryDemo.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.mlcrosoft.com/wlnfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/wlnfx/2006/xaml"> <Path Fill="Blue" Stroke="Red" StrokeTh1ckness="3"> <Path.Data> <E111pseGeometry Center="196. 144" RadiusX="144" Rad1usY="96" /> </Path.Data> </Path> </Canvas> Вспомните программу DrawCircles из главы 9, в которой на панели Canvas рисовались круги мышью. Начальный щелчок определял позицию центра круга, а перемещение мыши управляло радиусом. Но в этой программе используется элемент Ellipse, который требует, чтобы размер эллипса определялся шириной и высотой. Местоположение эллипса на панели Canvas задается присоединенны- ми свойствами Left и Тор. Когда программа DrawCircles реагирует на перемеще- ния мыши увеличением или уменьшением круга, она должна сохранить цен- тровку эллипса в исходной точке, для чего приходится как перемещать эллипс, так и изменять его размеры. Использование класса EllipseGeometry упростило бы логику программы. Элемент свойства Path.Data может содержать только одного потомка типа Geometry. Как вы уже видели, это ограничение обходится посредством исполь- зования нескольких элементов Path. Кроме того, элементы Path могут использо- ваться в сочетании с другими элементами (например, Rectangle). Ограничение можно обойти и другим способом — сделать объект Geometry- Group дочерним по отношению к Path.Data. Класс GeometryGroup является произ- водным от Geometry, но обладает свойством Children типа Geometrycollection (кол- лекция объектов Geometry). Объект GeometryGroup может содержать несколько дочерних объектов Geometry. Разметка выглядит примерно так: <Path F111="Gold" Stroke="P1nk" ...> <Path.Data> <GeometryGroup>
Классы Geometry и Path 805 <Е111pseGeometry ... /> <LineGeometry ... /> <RectangleGeometry ... /> </GeometryGroup> </Path.Data> </Path> Поскольку все объекты Geometry в GeometryGroup являются частью одного элемента Path, они обладают одинаковыми кистями Stroke и Fill. В этом заключа- ется первое принципиальное отличие GeometryGroup от использования несколь- ких элементов Path. Другое принципиальное различие представлено в следующем XAML-файле. Слева изображены два перекрывающихся прямоугольника, нарисованных с ис- пользованием двух отдельных элементов Path. Два прямоугольника справа явля- ются частью одной группы GeometryGroup. OverlappingRectangles.xaml <।-- ======================================================== OverlappingRectangles.xaml (с) 2006 by Charles Petzold Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Path Fill="Gold" Stroke="Red" StrokeThickness-"3"> <Path.Data> <RectangleGeometry Rect="96 96 192 192" /> </Path.Data> </Path> <Path Fill="Gold" Stroke="Red" StrokeThickness="3"> <Path.Data> <RectangleGeometry Rect="192 192 192 192" /> </Path.Data> </Path> <Path Fill="Gold" Stroke="Red" StrokeThickness="3"> <Path.Data> <GeometryGroup> <RectangleGeometry Rect="480 96 192 192" /> <RectangleGeometry Rect="576 192 192 192" /> </GeometryGroup> </Path.Data> </Path> </Canvas> В первой паре один прямоугольник просто располагается поверх другого. Но во второй паре прямоугольников, объединенных в GeometryGroup, область пе- рекрытия прозрачна. Здесь происходит то же самое, что и с элементом Polygon,
806 Глава 28. Классы Geometry и Path имеющим перекрывающиеся границы. GeometryGroup определяет собственное свойство FillRule, а в режиме по умолчанию EvenOdd не заполняются замкнутые области, отделяемые от бесконечности четным количеством границ. Попробуйте задать FillRule значение NonZero: <GeometryGroup Fil lRule="NonZero"> В этом случае перекрывающаяся область будет заполнена, но вторая пара прямоугольников все равно отличается от первой. В первой паре второй прямо- угольник закрывает часть контура первого прямоугольника, а во второй паре видны все границы. Таким образом, объединение нескольких геометрий в одном объекте GeometryGroup позволяет создавать интересные визуальные эффекты. FourOverlappingCircles.cs <I_ _ ========================================================= FourOverlapplngCircles.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Path Fill-'Blue" Stroke="Red" StrokeThickness="3"> <Path.Data> <GeometryGroup> <EllipseGeometry Center="150 150" RadiusX="100" RadiusY="100" /> <EllipseGeometry Center-"250 150" RadiusX="100" RadiusY="100" /> <EllipseGeometry Center="150 250" RadiusX="100" RadiusY="100" /> <EllipseGeometry Center="250 250" RadiusX="100" RadiusY="100" /> </GeometryGroup> </Path.Data> </Path> </Canvas> Класс GeometryGroup легко спутать с другим потомком Geometry, который называется CombinedGeometry. Впрочем, класс CombinedGeometry заметно отлича- ется от него. Во-первых, он не имеет свойства Children; его заменяют два свойст- ва с именами Geometryl и Geometry?. Класс CombinedGeometry объединяет два — и только два! — объекта Geometry. Второе отличие заключается в том, что CombinedGeometry не имеет свойства FillRule. Вместо него используется свойст- во GeometryCombineMode, которому задается одно из значений из перечисления GeometryCombineMode: Union, Intersect, Хог или Exclude. Например, значение Exclude создает геометрический объект, состоящий из всех частей Geometry?, не входя- щих в Geometryl. Следующий автономный XAML-файл демонстрирует четыре режима Geometry- CombineMode на примере двух перекрывающихся кругов.
Классы Geometry и Path 807 CombinedGeometryModes.xaml <! - - ================================================= CombinedGeometryModes.xaml (c) 2006 by Charles Petzold <Un1formGrid xmlns="http://schemas.mlcrosoft.com/wlnfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/wlnfx/2006/xaml" Rows="2" Columns="2" TextBlock.FontS1ze="12pt"> < Un1formGrid.Resources> <Style TargetType="{x:Type Path}"> <Setter Property="Hor1zontalA11gnment" Value="Center" /> <Setter Property="Vert1calA11gnment" Value="Center" /> <Setter Property=,,F1H" Value="Blue" /> <Setter Property="Stroke" Value="Red" /> <Setter Property="StrokeTh1ckness" Value="5" /> </Style> < /Un1formGrid.Resources> < !-- GeometryComblneMode = "Union". --> < Gr1d> <TextBlock HorlzontalAl1gnment="Center"> Geomet ryCombineMode="Uni on" </TextBlock> <Path> <Path.Data> <Comb1nedGeometry GeometryComb1neMode="Un1on"> <Comb1nedGeometry.Geometry1> <E111pseGeometry Center="96 96" Rad1usX="96" Rad1usY="96" /> < /CombinedGeometry.Geometry 1> < Comb1nedGeometry.Geometry2> <E111pseGeometry Center="48 48" Rad1usX="96" Rad1usY="96" /> < /Comb1nedGeometry.Geometry2> </Comb1nedGeometry> </Path.Data> </Path> </Gr1d> <!-- GeometryComblneMode = "Intersect". --> <Gr1d> <TextBlock HorlzontalAl1gnment="Center"> GeometryComb1neMode="Intersect" </TextBlock> <Path> <Path.Data> <Comb1nedGeometry GeometryComb1neMode="Intersect">
808 Глава 28. Классы Geometry и Path <Comb1nedGeometry.Geometryl> <E111pseGeometry Center="96 96" Rad1usX="96" Rad1usY="96" /> </Comb1nedGeometry.Geometry1> <Comb1nedGeometry.Geometry2> <E111pseGeometry Center="48 48" Rad1usX="96" Rad1usY="96" /> </Comb1nedGeometry.Geometry2> </Comb1nedGeometry> </Path.Data> </Path> </Grid> <!-- GeometryCombineMode = "Xor". --> <Gr1d> <TextBlock Horizontal Al1gnment="Center"> GeometryComb1neMode="Xor" </TextBlock> <Path> <Path.Data> <Comb1nedGeometry GeometryCombineMode="Xor"> <Comb1nedGeometry.Geometry1> <E111pseGeometry Center="96 96" Rad1usX="96" Rad1usY="96" /> </Comb1nedGeometry.Geometryl> <Comb1nedGeometry.Geometry2> <E111pseGeometry Center="48 48" Rad1usX="96" Rad1usY="96" /> </Comb1nedGeometry.Geometry2> </Comb1nedGeometry> </Path.Data> </Path> </Gr1d> <!-- GeometryCombineMode = "Exclude". --> <Gr1d> <TextBlock Horizontal Al1gnment="Center"> GeometryComblneMode="Excl ude" </TextBlock> <Path> <Path.Data> <Comb1nedGeometry GeometryCombineMode="Excl ude"> <Comb1nedGeometry.Geometry1> <E111pseGeometry Center="96 96" Rad1usX="96" Rad1usY="96" /> </Comb1nedGeometry.Geometry1> <Comb1nedGeometry.Geometry2> <E111pseGeometry Center="48 48"
Классы Geometry и Path 809 Rad1usX="96" Rad1usY="96" /> </Combi nedGeometry.Geometry2> </Comb1 nedGeometry> </Path.Data> </Path> </Gr1d> </Un1formGr1d> Эти изображения отличаются от всех комбинаций объектов, которые встре- чались нам до настоящего момента. Даже вариант Union — который вроде бы не дает никаких преимуществ по сравнению с простым наложением фигур — при- водит к новому результату: граница объединенных кругов проходит по состав- ному объекту, а не по оригиналам. Класс CombinedGeometry позволяет добиться некоторых эффектов, которые было бы трудно воспроизвести иным способом. Давайте посмотрим, как с его помощью нарисовать гантель. Dumbbell.xaml <! -- =========================================== Dumbbell.xaml (с) 2006 by Charles Petzold Canvas xmlns="http://schemas.m1crosoft.com/w1nfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml"> <Path F111="DarkGray" Stroke="Black" StrokeTh1ckness="5"> <Path.Data> <Comb1nedGeometry GeometryCombineMode="Un1 on"> <Comb1nedGeometry.Geometry1> <Comb1nedGeomet ry Geomet ryComb1neMode="Un1 on"> < Comb1nedGeometry.Geometry1> <E111pseGeometry Center="100 100" Rad1usX="50" Rad1usY="50" /> < /Comb1nedGeometry.Geometry1> < Comb1nedGeometry.Geometry2> <RectangleGeometry Rect="100 75 200 50" /> < /Comb1nedGeometry.Geometry2> </Comb1nedGeometry> < /Comb1nedGeometry.Geometry1> < Comb1nedGeometry.Geometry2> <E111pseGeometry Center="300 100" Rad1usX="50" Rad1usY="50" /> < /Comb1nedGeometry.Geometry2> </Comb1nedGeometry> </Path.Data> </Path> </Canvas> В изображении используются два вложенных объекта CombinedGeometry. Пер- вый объект объединяет эллипс с прямоугольником, а второй объединяет получен- ный результат с другим эллипсом. Все три объекта объединяются в режиме Union.
810 Глава 28. Классы Geometry и Path Однако возможности иерархии Geometry в полной мере раскрываются лишь в производном классе с именем PathGeometry. Графическая траектория представ- ляет собой совокупность отрезков и кривых, соединенных или не соединенных друг с другом. Любой набор соединенных отрезков и кривых внутри траектории называется субтраекторией, или фигурой. Таким образом, траектория состоит из нуля или более фигур. Любая фигура может быть либо замкнутой, либо открытой. Фигура считает- ся замкнутой, если конец ее последней линии соединяется с началом первой ли- нии. В противном случае фигура считается открытой. (Традиционно открытые траектории не могли заполняться кистью; в WPF заполнение производится не- зависимо от замыкания.) Начнем с краткой сводки: объект PathGeometry состоит из одного или не- скольких объектов PathFigure. Каждый объект PathFigure состоит из соединенных объектов PathSegment. Объект PathSegment представляет отдельный отрезок или сегмент кривой. Теперь перейдем к деталям. Класс PathGeometry определяет свойство FillRule, которое управляет заполнением фигур, входящих в траекторию, и свойство Figures типа PathFigureCollection — коллекцию объектов PathFigure. Тип PathFigure является производным от Animatable и фиксируется (то есть другие классы не могут быть производными от PathFigure). В нем определяют- ся два логических типа с именами IsClosed и IsFilled. По умолчанию свойство IsClosed равно false. Если задать ему true, фигура автоматически замыкается, то есть к завершающей точке фигуры добавляется прямая линия, соединяющая ее с начальной точкой. Свойство IsFilled по умолчанию равно true; оно управляет заполнением внутренних областей кистью. Область заполняется как замкнутая даже в том случае, если она замкнутой не является (нечто похожее происходило в главе 27 при заполнении областей Polyline). Итак, фигура представляет собой совокупность соединенных отрезков и сег- ментов кривых. Фигура начинается в определенной точке, которая задается свойством StartPoint класса PathFigure. Класс PathFigure также определяет свойство Segments типа PathSegmentCollection — коллекции объектов PathSegment. Абстракт- ный класс PathSegment имеет семь производных классов. Все классы, связанные с траекториями, показаны в следующей иерархии: Object Di spatcherObject (абстрактный) Dependencyobject Freezable (абстрактный) Animatable (абстрактный) PathFigure PathFigureCollection PathSegment (абстрактный) ArcSegment BezlerSegment LlneSegment PolyBezlerSegment
Классы Geometry и Path 811 PolyLineSegment PolyQuadrati cBezi erSegnient Quadrati cBezi erSegnient PathSegmentCol1ectlon Вас интересовало, где в графической системе WPF скрыты кривые Безье? Они перед вами, притом двух видов: обычные кубические и более быстрые квадра- тичные. Дуги (ArcSegment) определяются как части границы эллипса. Только эти классы WPF обладают прямой поддержкой кривых Безье и дуг. Даже при выводе графики на уровне DrawingContext (методом OnRender или с созданием объ- екта DrawingVisual) методы рисования кривых Безье и дуг недоступны. Для рисо- вания последних приходится использовать метод DrawGeometry или DrawDrawing. Классы, производные от PathSegment, также предоставляют единственный спо- соб создания объектов Geometry, описывающих произвольные многоугольники. Теоретически многоугольники можно «склеить» при помощи CombinedGeometry и поворотов, но, скорее всего, подобные попытки приведут вас к помешательству. В объекте PathFigure начальная точка обозначается свойством StartPoint. Таким образом, класс LineSegment содержит единственное свойство Point, которому за- дается конечная точка лилии. Следующая тривиальная траектория состоит из единственной прямой линии. TrivialPath.xaml <i__ ============================================== Trivial Path.xaml (c) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Path Stroke="Blue" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathFigure StartPoint="96 96"> <LineSegment Point="384 192" /> </PathFigure> </PathGeometry> </Path.Data> </Path> </Canvas> Для рисования нескольких соединенных отрезков можно использовать не- сколько объектов LineSegment. Каждый объект LineSegment продолжает фигуру очередным сегментом, начинающимся от конца предыдущего сегмента. Следую- щий XAML-файл рисует пятиконечную звезду из четырех объектов LineSegment. MultipleLineSegments.xaml <!_ _ ======================================================= MultipleLineSegments.xaml (с) 2006 by Charles Petzold
812 Глава 28. Классы Geometry и Path <Canvas xmlns="http://schemas.mlcrosoft.com/wlnfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/wlnfx/2006/xaml"> <Path F111="Aqua" Stroke="Maroon" StrokeTh1ckness="3"> <Path.Data> <PathGeometry> <PathF1gure StartPo1nt="144 72"> <L1neSegment Po1nt="200 246" /> <L1neSegment Po1nt="53 138" /> <L1neSegment Po1nt="235 138" /> <L1neSegment Po1nt="88 246" /> </PathF1gure> </PathGeometry> </Path.Data> </Path> </Canvas> Нарисованная звезда выглядит немного странно. Пять ее лучей заполнены, а центр остался незаполненным, как и следовало ожидать от используемого по умолчанию режима EvenOdd, однако линия от левой нижней точки к верхней от- сутствует. А присмотревшись к разметке, мы видим, что четыре объекта Line- Segment рисуют только четыре линии. Чтобы придать звезде нормальный вид, можно включить пятый объект LineSegment с точкой, заданной в PathFigure: <L1neSegment Po1nt="144 72" /> Также можно задать свойству IsClosed элемента PathFigure значение true: <PathF1gure StartPo1nt="144 72" IsClosed="True"> С истинным свойством IsClosed включается дополнительный отрезок, ве- дущий к начальной точке. Существование последнего не влияет на заполнение внутренней области. Решение о заполнении внутренних областей принимается в зависимости от свойства IsFilled объекта PathFigure (по умолчанию true) и атри- бута FillRule элемента Path (EvenOdd по умолчанию). Другой способ рисования серии соединенных отрезков основан на использо- вании объекта PolyLineSegment. Свойство Points, определяемое PolyLineSegment, яв- ляется объектом типа Pointcollection; как и в случае со свойством Points, опреде- ляемым элементом Polyline, в XAML ему задается строка с координатами точек. В следующем файле я явно задал свойству IsClosed объекта PathFigure значение true: PolyLineSegmentDemo.xaml <।_ _ ====================================================== PolyLineSegmentDemo.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.mlcrosoft.com/wlnfx/2006/xaml/presentatlon" xml ns:x="http://schemas.mlcrosoft.com/wlnfx/2006/xaml"> <Path F111="Aqua" Stroke="Maroon" StrokeTh1ckness="3"> <Path.Data> <PathGeometry>
Классы Geometry и Path 813 <PathF1gure StartPo1nt="144 72" IsClosed="True"> <PolyL1neSegment Po1nts= "200 246, 53 138, 235 138, 88 246" /> </PathF1gure> </PathGeometry> </Path.Data> </Path> </Canvas> Если бы объект PathFigure содержал дополнительный сегмент, следующий за PolyLineSegment, он бы шел от последней точки PolyLineSegment. Элемент PathFigure всегда определяет начало фигуры, а каждый сегмент содержит одну или несколь- ко точек, продолжающих фигуру от предыдущей точки. Свойство FillRule определяется пятью классами: Polyline, Polygon, GeometryGroup, PathGeometry и StreamGeometry. С классом PathGeometry свойство FillRule также управляет заполнением нескольких фигур, входящих в геометрический объект. В следующем примере две перекрывающихся звезды объединяются в одном объекте PathGeometry. OverlappingStars.xaml <i __ =================================================== OverlappingStars.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml"> <Path F111="Aqua" Stroke="Maroon" StrokeTh1ckness="3"> <Path.Data> <PathGeometry> <PathF1gure StartPo1nt="144 72" IsClosed="True"> <PolyL1neSegment Po1nts= "200 246, 53 138, 235 138, 88 246" /> </PathF1gure> <PathF1gure StartPo1nt="168 96" IsClosed="True"> <PolyL1neSegment Po1nts= "224 260, 77 162, 259 162, 112 270" /> </PathF1gure> </PathGeometry> </Path.Data> </Path> </Canvas> Вторая звезда сдвинута от первой на четверть дюйма по горизонтали и верти- кали. Вы увидите, что при выборе внутренних областей, заполняемых по правилу FillRule или EvenOdd, учитываются обе звезды. Если заключить объект PathGeometry в GeometryGroup, предпочтение отдается режиму FillRule объекта GeometryGroup, и режим FillRule объекта PathGeometry игнорируется. При определении заполняе- мых областей по-прежнему будут анализироваться обе звезды. Если вы хотите, чтобы перекрывающиеся траектории заполнялись независимо друг от друга, разместите их в разных элементах Path.
814 Глава 28. Классы Geometry и Path Класс ArcSegment определяет кривую линию на границе эллипса — на прак- тике эта концепция оказывается более сложной, чем кажется на первый взгляд. Как и LineSegment, класс ArcSegment определяет всего одну точку, и дуга рисуется из последней предшествующей точки в точку, заданную в элементе ArcSegment. Тем не менее элемент ArcSegment должен содержать дополнительную информа- цию: так как кривая, соединяющая две точки, лежит на границе эллипса, необ- ходимо указать две полуоси эллипса. В простейшем случае их величины равны, а кривая, соединяющая две точки, в действительности является частью окруж- ности. Но в общем случае дугу, соединяющую две точки на границе эллипса, можно провести четырьмя способами. Выбор способа определяется свойством SweepDirection класса ArcSegment, которому задается значение Clockwise или Counter- clockwise (по умолчанию), а также логическим свойством IsLargeArc (false по умол- чанию). В следующей программе четыре дуги выделены разными цветами. ArcPossibilities.xaml <।_ _ ============================================= ArcPossibilities.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml"> < !-- Counterclockwise (по умолчанию). малая дуга (по умолчанию) --> <Path Stroke="Red" StrokeTh1ckness="3"> <Path.Data> <PathGeometry> <PathF1gure StartPo1nt="144 144"> <ArcSegment Po1nt="240 240" S1ze="96 96" /> </PathF1gure> </PathGeometry> </Path.Data> </Path> < !-- Counterclockwise (по умолчанию), IsLargeArc --> <Path Stroke="Blue" StrokeTh1ckness="3"> <Path.Data> <PathGeometry> <PathF1gure StartPo1nt="144 144"> <ArcSegment Po1nt="240 240" S1ze="96 96" IsLargeArc="True" /> </PathF1gure> </PathGeometry> </Path.Data> </Path> < !-- Clockwise, малая дуга (по умолчанию) --> <Path Stroke="Green" StrokeTh1ckness="3"> <Path.Data>
Классы Geometry и Path 815 <PathGeometry> <PathF1gure StartPo1nt="144 144"> <ArcSegment Po1nt="240 240" S1ze="96 96" SweepDirection="ClockWIse" /> </PathF1gure> </PathGeometry> </Path.Data> </Path> <!-- Clockwise, IsLargeArc > <Path Stroke="Purple" StrokeTh1ckness="3"> <Path.Data> <PathGeometry> <PathF1gure StartPo1nt="144 144"> <ArcSegment Po1nt="240 240" S1ze="96 96" SweepDirection="ClockWIse" IsLargeArc="True" /> </PathF1gure> </PathGeometry> </Path.Data> </Path> </Canvas> Все четыре элемента Path используют одну начальную точку (144, 144) и точ- ку ArcSegment (240, 240). Во всех четырех случаях точки соединяются дугой ок- ружности радиусом 96. Четыре разных комбинации SweepDirection и IsLargeArc представлены четырьмя дугами, нарисованными разными цветами. А если этого недостаточно, ArcSegment определяет еще одно свойство Rotation- Angle, которое определяет угол поворота эллипса (по часовой стрелке). Следую- щая программа идентична ArcPossibilities.xaml, но в ней используется эллипс с го- ризонтальной полуосью 144 и вертикальной полуосью 96, повернутый на 45°. ArcPossibilities2.xaml <।_. ==================================================== ArcPosslbl 11ties2.xaml (c) 2006 by Charles Petzold <Canvas xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml"> <!-- Counterclockwise (по умолчанию), малая дуга (по умолчанию) --> <Path Stroke="Red" StrokeTh1ckness="3"> <Path.Data> <PathGeometry> <PathF1gure StartPo1nt="144 144"> <ArcSegment Po1nt="240 240" S1ze="144 96" Rotat1onAngle="45" /> </PathF1gure> </PathGeometry> </Path.Data> </Path>
816 Глава 28. Классы Geometry и Path < !-- Counterclockwise (по умолчанию), IsLargeArc --> <Path Stroke="Blue" StrokeTh1ckness="3"> <Path.Data> <PathGeometry> <PathF1gure StartPo1nt="144 144"> <ArcSegment Po1nt="240 240" S1ze="144 96" Rotat1onAngle="45" IsLargeArc="True" /> </PathF1gure> </PathGeometry> </Path.Data> </Path> < !-- Clockwise, малая дуга (по умолчанию) --> <Path Stroke="Green" StrokeTh1ckness="3"> <Path.Data> <PathGeometry> <PathF1gure StartPo1nt="144 144"> <ArcSegment Point="240 240" S1ze="144 96" Rotat1onAngle="45" SweepDIrection="ClockW1se" /> </PathF1gure> </PathGeometry> </Path.Data> </Path> < !-- Clockwise, IsLargeArc --> <Path Stroke="Purple" StrokeTh1ckness="3"> <Path.Data> <PathGeometry> <PathF1gure StartPo1nt="144 144"> <ArcSegment Po1nt="240 240" S1ze="144 96" Rotat1onAngle="45" SweepDIrection="ClockW1se" IsLargeArc="True" /> </PathF1gure> </PathGeometry> </Path.Data> </Path> </Canvas> Следующая программа больше похожа на реальное приложение. Она рисует нечто похожее на фрагмент воображаемой машины — контур из прямых линий и дуг, а также отверстие, образованное из двух дуговых сегментов. FigureWithArcs.xaml < I _ _ ============================================;===== FigureWithArcs.xaml (с) 2006 by Charles Petzold
классы Geometry и Path 817 <Canvas xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml"> <Path F111="Aqua" Stroke="Maroon" StrokeTh1ckness="3"> <Path.Data> <PathGeometry> <PathF1gure StartPo1nt="192 192"> <ArcSegment Po1nt="192 288" S1ze="48 48" /> <L1neSegment Po1nt="480 288" /> <ArcSegment Po1nt="480 192" S1ze="48 48" /> <L1neSegment Po1nt="384 192" /> <ArcSegment Po1nt="288 192" S1ze="48 48" /> <L1neSegment Po1nt="192 192" /> </PathF1gure> <PathF1gure StartPo1nt="336 200" IsClosed="True"> <ArcSegment Po1nt="336 176" S1ze="12 12" /> <ArcSegment Po1nt="336 200" S1ze="12 12" /> </PathF1gure> </PathGeometry> </Path.Data> </Path> </Canvas> Второй элемент PathFigure фактически рисует круг, соединяя две полуокруж- ности. Кривая Безье представляет собой сплайн, то есть кривую, используемую для аппроксимации дискретных данных в виде плавной непрерывной функции. Кубическая кривая Безье однозначно определяется четырьмя точками р0, р2 и р3. Кривая начинается в точке р0 и завершается в р3. Точка р0 часто называется начальной точкой кривой, а р3— конечной точкой, хотя иногда обе точки объе- диняются общим термином «конечные точки». Точки и р2 называются кон- трольными. На следующем рисунке приводится пример кривой Безье с двумя конечными и двумя контрольными точками. Р\ Р2
818 Глава 28. Классы Geometry и Path Кривая начинается в точке р{} и направляется к но затем плавно меняет направление па р2. Не соприкасаясь с /л, кривая завершается в точке рл. А вот другой пример кривой Безье. Ри Ра Кривые Безье крайне редко проходят через контрольные точки. Впрочем, если разместить обе контрольные точки между конечными, кривая Безье пре- вращается в прямую и проходит через них. До Р1 Р'2 Р'А Возможна и другая крайность, когда кривая Безье описывает небольшую петлю. • • Р> Р\ Для рисования одной кривой Безье необходимы четыре точки. В контексте PathFigure первая точка (р0 на диаграмме) определяется либо свойством StartPoint объекта PathFigure, либо последней точкой предшествующего сегмента. Две кон- трольные точки и конечная точка задаются свойствами Pointl, Point2 и Point3 класса BezierSegment. Нумерация этих свойств соответствует нумерации точек на диаграммах. Следующая XAML-программа создает изображение, напоминающее первую диаграмму Безье на предыдущей странице. SingleBezier.xaml <।_ _ =============================================== SingleBezier.xaml (с) 2006 by Charles Petzold Canvas xmlns="http://schemas.mlcrosoft.com/wlnfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/wlnfx/2006/xaml"> <Path F111="Red" Stroke="Blue" StrokeTh1ckness="3">
Классы Geometry и Path 819 <Path.Data> <GeometryGroup> <PathGeometry> <PathFigure x:Name="fig" StartPoint="50 150" IsFilled="False" > <BezierSegment Pointl="25 25" Point2="400 300" Point3="450 150" /> </PathFigure> </PathGeometry> <EllipseGeometry Center="{Binding ElementName=fig, Path=StartPoint}" RadiusX="5" RadiusY="5" /> <EllipseGeometry Center="{Binding ElementName=fig, Path=Segments[O].Pol nt1}" RadiusX="5" RadiusY="5" /> <EllipseGeometry Center="{Binding ElementName=fig, Path=Segments[O].Polnt2}" RadiusX="5" RadiusY="5" /> <E11jpseGeometry Center="{Binding ElementName=fig, Path=Segments[O].Polnt3}" RadiusX="5" RadiusY="5" /> </GeometryGroup> </Path.Data> </Path> </Canvas> К кривой Безье были добавлены четыре объекта EllipseGeometry с привязками для обозначений позиций конечных и контрольных точек. Кривые Безье нашли широкое применение в графическом программировании по нескольким причинам. Все эти причины становятся очевидными, стоит лишь немного поэкспериментировать с кривыми. Для этой цели был написан проект BezierExperimenter, состоящий из XAML-файла и файла С#. XAML-файл строит макет панели Canvas, начиная с четырех объектов Ellipse- Geometry. Эти объекты изображают четыре точки, определяющие кривую Безье, а присвоенные им атрибуты x:Name становятся именами переменных, исполь- зуемыми в файле C# для задания точек. BezierExperimenter.xaml BezierExperimenter.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" x:Class="Petzold.Bezi erExperimenter.Bezi erExperi menter" Title="Bezier Experimenter"> <Canvas Name="canvas">
820 Глава 28. Классы Geometry и Path <!-- Рисование четырех точек, определяющих кривую --> <Path F111="{Dynam1cResource {х:Static SystemColors.WIndowTextBrushKey}}"> <Path.Data> <GeometryGroup> <EllipseGeometry x:Name="ptStart" Rad1usX="2" Rad1usY="2" /> <E111pseGeometry x:Name="ptCtrH" Rad1usX="2" Rad1usY="2" /> <E111pseGeometry x:Name="ptCtrl2" Rad1usX="2" Rad1usY="2" /> <E111pseGeometry x:Name="ptEnd" Rad1usX="2" Rad1usY="2" /> </GeometryGroup> </Path.Data> </Path> <!-- Рисование кривой --> <Path Stroke="{DynamicResource {x:Static SystemColors.WIndowTextBrushKey}}"> <Path.Data> <PathGeometry> <PathGeometry.F1gures> <PathF1gure StartPo1nt="{B1nd1ng ElementName=ptStart, Path=Center}"> <Bez1erSegment Po1ntl="{B1nd1ng ElementName=ptCtrl1, Path=Center}" Polnt2="{Blndlng ElementName=ptCtrl2, Path=Center}" Polnt3="{Blndlng ElementName=ptEnd, Path=Center}" /> </PathF1gure> </PathGeometry.Flgures> </PathGeometry> </Path.Data> </Path> <!-- Рисование серых линий, соединяющих конечные и контрольные точки--> <Path Stroke="{DynamicResource {х:Static SystemColors.GrayTextBrushKey}}"> <Path.Data> <GeometryGroup> <L1neGeometry StartPo1nt="{B1nd1ng ElementName=ptStart, Path=Center}" EndPolnt="{Bind!ng ElementName=ptCtrl1, Path=Center}" /> <L1neGeometry StartPo1nt="{B1nd1ng ElementName=ptEnd,
Классы Geometry и Path 821 Path=Center}" EndPoint="{Blnding ElementName=ptCtrl2, </GeometryGroup> </Path.Data> </Path> <!-- Вывод надписей с координатами точек --> <Label Canvas.Left="{Blnding ElementName=ptStart. Path=Center.X}" Canvas.Top="{B1nd1ng ElementName=ptStart. Path=Center.Y}" Content="{Binding ElementName=ptStart, Path=Center}" /> <Label Canvas.Left="{B1nd1ng ElementName=ptCtrll, Path=Center.X}" Canvas.Top="{B1nd1ng ElementName=ptCtrll, Path=Center.Y}" Content="{B1nd1ng ElementName=ptCtrll. Path=Center}" /> <Label Canvas.Left="{Blnding ElementName=ptCtrl2. Path=Center.X}" Canvas.Top="{B1nd1ng ElementName=ptCtrl2, Path=Center.Y}" Content="{B1nd1ng ElementName=ptCtrl2. Path=Center}" /> <Label Canvas.Left="{B1nd1ng ElementName=ptEnd, Path=Center.X}" Canvas.Top="{B1nd1ng ElementName=ptEnd, Path=Center.Y}" Content="{B1nd1ng ElementName=ptEnd, Path=Center}" /> </Canvas> </W1ndow> Панель Canvas продолжается определением группы привязок, ссылающихся на свойства Center четырех объектов EllipseGeometry. PathSegment рисует кривую Безье, а две серые линии соединяют две конечные точки с двумя контрольными точками (вскоре вы поймете, зачем это делается). Наконец, на рисунке выводят- ся надписи с координатами точек — на случай, если вы создадите красивую кри- вую Безье, которую вам захочется использовать в разметке XAML. Часть программы, написанная на С#, сбрасывает позиции четырех точек при любых изменениях размеров окна. BezierExperimenter.cs // И Bez1erExper1menter.cs (с) 2006 by Charles Petzold // using System: using System.Windows; using System.Windows.Controls; using System.Windows.Input: using System.Windows.Media; namespace Petzold.BezlerExperlmenter { public partial class BezlerExperlmenter : Window { [STAThread] public static void MalnO
822 Глава 28. Классы Geometry и Path { Application app = new Application): app.Run(new Bez1erExper1menter()): } public Bez1erExper1menter() { Initial1zeComponent(): canvas.SIzeChanged += CanvasOnSIzeChanged; } // При изменении размеров Canvas сбросить четыре точки protected virtual void Canvas0nS1zeChanged(object sender, SIzeChangedEventArgs args) { ptStart.Center = new Po1nt(args.NewSize.Width / 4, args.NewSize.Helght /2): ptCtrll.Center = new Pol nt(args.NewSize.Width / 2, args.NewSize.Height / 4): • ptCtr12.Center = new Po1nt(args.NewSize.Width / 2, 3 * args.NewSize.Helght /4); ptEnd.Center = new Po1nt(3 * args.NewSize.Width / 4, args.NewSIze.Helght / 2): } // Изменение контрольных точек на основании // щелчков и перемещений мыши protected override void OnMouseDown(MouseButtonEventArgs args) { base.OnMouseDown(args); Point pt = args.GetPositlon(canvas): If (args.ChangedButton == MouseButton.Left) ptCtrll.Center = pt: If (args.ChangedButton == MouseButton.Right) ptCtrl2.Center = pt: protected override void OnMouseMove(MouseEventArgs args) { base.OnMouseMove(args); Point pt = args.GetPosltlon(canvas): If (args.LeftButton == MouseButtonState.Pressed) ptCtrll.Center = pt: If (args.RIghtButton == MouseButtonState.Pressed) ptCtrl2.Center = pt; } } } Две конечные точки всегда остаются в фиксированной позиции по отношению к окну. Левая кнопка мыши управляет первой контрольной точкой, а правая -
Классы Geometry и Path 823 второй. Поэкспериментируйте с программой, и вы убедитесь, что при некотором навыке кривой легко придать нужную форму. Некоторые сплайны не проходят ни через одну из точек, определяющих их; кривые Безье всегда привязаны к двум конечным точкам. Некоторые разновид- ности сплайнов имеют аномалии, при которых кривая уходит в бесконечность — весьма нежелательный эффект для компьютерного проектирования. С другой стороны, кривая Безье всегда ограничена четырехсторонним многоугольником, образованным соединением конечных и контрольных точек. (Последовательность соединения точек зависит от конкретной кривой.) Вы можете определить нескольких соединенных кривых Безье, задав свойство Points элемента PolyBezierSegment. Хотя количество точек не ограничено, на прак- тике имеют смысл лишь величины, кратные трем: первая и вторая точки — кон- трольные, третья — конечная. Третья точка также является начальной точкой следующей кривой Безье (если она есть), четвертая и пятая — контрольные точ- ки второй кривой и т. д. Хотя соединенные кривые Безье обладают общими конечными точками, пе- реход в точке соединения двух кривых не обязан быть гладким. С математиче- ской точки зрения составная кривая считается гладкой только в том случае, если ее первая производная непрерывна. Если вы хотите, чтобы переход между двумя соединенными кривыми Безье был плавным, вторая контрольная точка первой кривой, конечная точка первой кривой (которая также является началь- ной точкой второй кривой) и первая контрольная точка второй кривой должны лежать на одной линии. Следующий XAML-файл демонстрирует стандартный прием рисования кру- га из четырех соединенных кривых Безье. SimulatedCircle.xaml < । _ _ ================================================== SimulatedCircle.xaml (с) 2006 by Charles Petzold <Canvas xml ns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Path Canvas.Left="150" Canvas.Top="150" Stroke="Black"> <Path.Data> <PathGeometry> <PathGeometry.Fi gures> <PathFigure StartPoint="0 100"> <PolyBezierSegment Points=" 55 100. 100 55. 100 0 100 -55. 55 -100. 0 -100 -55 -100. -100 -55. -100 0 -100 55, -55 100. 0 100" /> </PathFigure> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path> </Canvas>
824 Глава 28. Классы Geometry и Path Радиус окружности, определяемой массивом Points, равен 100, а ее центр на- ходится в точке (0, 0). Присоединенные свойства Canvas.Left и Canvas.Top пере- мещают объект Path в более удобное место. Следующая программа использует описанный прием для рисования сим- вола бесконечности. Нарисованный знак закрашивается линейной градиентной кистью. Infinity.xaml <।.. =================================. Infinity.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Path Canvas.Left-"150" Canvas.Top-"150" StrokeThickness="25"> <Path.Stroke> <Li nearGradi entBrush> <Li nearGradi entBrush.Gradi entStops> <GradientStop 0ffset="0.00" Color="Red" /> <GradientStop 0ffset="0.16" Color="Orange" /> <GradientStop 0ffset="0.33" Color="Yellow" /> <GradientStop 0ffset="0.50" Color="Green" /> <GradientStop 0ffset="0.67" Color="Blue" /> <GradientStop 0ffset="0.84" Color="Indigo" /> <GradientStop 0ffset="1.00" Color="Violet" /> </Li nearGradi entBrush.Gradi entStops> </L inea rGradi entBrush> </Path.Stroke> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigure StartPoint="0 -100"> <PolyBezierSegment Points=" -55 -100, -100 -55. -100 0. -100 55. -55 100, 0 100, 55 100, 100 50, 150 0, 200 -50. 245 -100. 300 -100, 355 -100, 400 -55, 400 0, 400 55, 355 100, 300 100, 245 100, 200 50, 150 0, 100 -50. 55 -100, 0 -100" /> </PathFigure> </PathGeometry.Fi gures> </PathGeometry> </Path.Data> </Path> </Canvas>
Классы Geometry и Path 825 Итак, мы вкратце рассмотрели всех потомков PathSegment. Настало время по- знакомиться с мини-языком траекторий (более официальное название — син- таксис разметки PathGeometry) — строкой, в которой перечислены все типы сег- ментов траектории. Строка задается атрибуту Data объекта Path или любому дру- гому свойству типа Geometry, которое может встретиться в других элементах. Для начала рассмотрим небольшой пример. PathMiniLanguage.xaml <!-- ===========================-======================= PathMiniLanguage.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Path Fill="Aqua" Stroke="Magenta" StrokeThickness="3" Data="M 50 75 L 250 75. 250 275, 50 275 Z" /> </Canvas> В мини-языке буквенные команды чередуются с числовыми параметрами. «М» означает перемещение (Move); это эквивалент свойства StartPoint объекта PathFigure. Точка имеет координаты (50, 75). Команда L рисует линию — в приве- денном примере будут нарисованы три линии из точки (50, 75) в точку (250, 75), затем в точку (250, 275) и в точку (50, 275). Команда Z в конце строки замыкает фигуру (в данном примере это квадрат со стороной 200 единиц). Далее может следовать команда «М», начинающая новую фигуру. Без нее но- вая фигура по умолчанию начнется в точке (50, 75) — последнего точке преды- дущей фигуры после ее замыкания. Команды Н и V рисуют соответственно горизонтальные и вертикальные ли- нии к заданной точке. Тот же квадрат можно нарисовать следующей последова- тельностью команд: Data="M 50 75 Н 250 V 275 Н 50 Z" Команды, записанные строчными буквами, делают то же самое, но не в абсо- лютных координатах, а в относительных. Следующая строка рисует тот же квад- рат, однако числа после h и v обозначают длину отрезка, а не координаты: Data="M 50 75 h 200 v 200 h -200 Z" Команда рисования линий тоже существует в версии, работающей в относи- тельных координатах: Data="M 50 75 1 200 0. О 200. -200 0 Z" Если вы хотите задать FillRule значение NonZero (вместо используемого по умолчанию EvenOdd), включите символы F1 в самое начало строки. (F0 означает значение по умолчанию.) Полный список команд мини-языка приведен в следующей таблице. Точка (х0> У о) является текущей, то есть изначально задается командой М, а затем соот- ветствует последней точке предшествующей команды.
826 Глава 28. Классы Geometry и Path Синтаксис Команда Описание Fi Правило заполнения i=0: EvenOdd i=l: NonZero Мху Перемещение Перемещение в точку (х,у) m x у Относительное перемещение Перемещение в точку (х0+х, уо+у) L x у Линия Рисование линии в точку (х, у) 1 x у Линия (относ.) Рисование линии в точку (х0+х, уо+у) Hx Горизонтальная линия Рисование линии в точку (х, у0) h х Горизонтальная линия (относ.) Рисование линии в точку (х0+х, у0) Vx Вертикальная линия Рисование линии в точку (х0/ у) vx Вертикальная линия (относ.) Рисование линии в точку (х0/ Уо+У) A xr yr a i j x у Дуга Рисование дуги в точку (х,у) на границе эллипса с полуосями (хг/ Уг), повернутого на а градусов. i=l: IsLargeArc; j=l: Clockwise a xr yr a i j x у Дуга (относ.) Рисование дуги в точку (х0+х, уо+у) C Xi yj x2 y2 x3 уз Кубическая кривая Безье Рисование кривой Безье в точку (х3/ у3) с контрольными точками (xb yj и (х2/ у2) C Xi У1 x2 y2 X3 Уз Кубическая кривая Безье (относ.) Рисование кривой Безье в точку (х0+х3, Уо+Уз) с контрольными точками (x0+Xi, y0+yi) и (х0+х2, у0+у2) s x2 y2 Хз Уз Гладкая кубическая кривая Безье Рисование кривой Безье в точку (х3/ у3) с контрольной точкой (х2/ у2) и отраженной контрольной точкой s x2 у2 Хз Уз Гладкая кубическая кривая Безье (относ.) Рисование кривой Безье в точку (х0+х3, Уо+Уз) с контрольной точкой (х0+х2, У0+У2) и отраженной контрольной точкой Q Xi У1 x2 y2 Квадратичная кривая Безье Рисование квадратичной кривой Безье в точку (х2, У2) с контрольной ТОЧКОЙ (Xi, yj q Xi yi x2 y2 Zz Квадратичная кривая Безье (относ.) Замыкание фигуры Рисование квадратичной кривой Безье в точку (Х0+Х2, У0+У2) С КОНТРОЛЬНОЙ ТОЧКОЙ (Xo+Xi, У0+У1) Гладкая кубическая кривая Безье плавно присоединяется к предыдущей кри- вой, для чего ее первая контрольная точка вычисляется автоматически. Данная возможность идеально подходит для рисования символа бесконечности, как по- казано в следующем примере. MiniLanguagelnfinity.xaml <।_ _ ======================================================= MinlLanguagelnfinity.xaml (с) 2006 by Charles Petzold
Классы Geometry и Path 827 <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Path Canvas.Left="150" Canvas.Top="150" Stroke="Black" Data="M 0 -100 C -55 -100. -100 -55. -100 0 S -55 100. 0 100 S 100 50. 150 0 S 245 -100. 300 -100 S 400 -55. 400 0 S 355 100. 300 100 S 200 50. 150 0 S 55 -100, 0 -100" /> </Canvas> Геометрические объекты применяются не только для рисования, но и для от- сечения. Класс UIEIement определяет свойство Clip типа Geometry, которому мож- но задать произвольный объект Geometry (или строку на мини-языке в XAML). Все части объекта Geometry, которые бы не заполнялись при его прорисовке, ото- бражаться не будут. Например, в следующем XAML-файле свойству Clip кнопки присваивается строка на мини-языке, определяющая четыре дуги. Первая пара дуг определяет эллипс с шириной и высотой кнопки, а вторая — маленький круг внутри эллипса. ClippedButton.xaml < | _ _ ==== = === ==== = = = = = =: = ==== ======= = = ====^== = === ===== ClippedButton.xaml (с) 2006 by Charles Petzold <Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Button HorizontalAlignment="Center" Vertical Alignment="Center" FontSize="24" Width="200" Height="100" Clip="M 0 50 A 100 50 0 0 0 200 50 A 100 50 0 0 0 0 50 M 90 50 A 10 10 0 0 0 110 50 A 10 10 0 0 0 90 50" > Clipped Button </Button> </Grid> В следующей программе знаменитое изображение с веб-сайта NASA отсека- ется по контуру замочной скважины. KeyholeOnTheMoon.xaml <! _ _ ==========3==========^============================== KeyholeOnTheMoon.xaml (с) 2006 by Charles Petzold <page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml">
828 Глава 28. Классы Geometry и Path <Image Source="http://images.jsc.nasa.gov/lores/ASll-40-5903.jpg" Clip="M 300 130 L 250 350 L 450 350 L 400 130 A 70 70 0 1 0 300 130" Stretch="None" /> </Page> Контуры шрифтов, используемых в системе Windows, задаются кубическими и квадратичными кривыми Безье. Процесс создания отображаемых символов шрифтов на основе этих кривых называется растеризацией. Однако шрифтовые файлы также содержат хинты («рекомендации»), чтобы из-за ошибок округле- ния, неизбежно возникающих в процессе растеризации, символы не становились нечитаемыми при малых размерах шрифтов, типичных для современных мони- торов. Впрочем, контуры символов можно получить в форме нехинтованных геомет- рических объектов и использовать их для реализации графических эффектов, недостижимых другими способами. Надеюсь, вы еще не забыли класс Formatted- Text? Он готовит текст к вызову метода DrawText класса DrawingContext. Этот класс содержит метод BuildGeometry, возвращающий объект типа Geometry с кон- турами символов текста. Впрочем, при использовании класса FormattedText прямо в XAML возникают некоторые проблемы. Конечно, объект FormattedText можно создать в виде ресурса, но BuildGeometry — метод, а не свойство, поэтому он не будет доступен для элементов XAML. Другое решение, которое приходит в голову, — определение класса, производ- ного от Geometry. Такой класс можно было бы использовать всюду, где присут- ствуют потомки Geometry. Но и такой способ невозможен — объем существую- щей документации не позволяет организовать наследование от самого класса Geometry, а все классы, производные от Geometry, зафиксированы. Столкнувшись с этими препятствиями, я написал простой класс со свойст- вами, соответствующими аргументам конструктора FormattedText, и свойством Geometry, вызывающим метод BuildGeometry для объекта FormattedText. TextGeometiy.cs //-------------------------------------------- // TextGeometry.cs (с) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Globalization; using System.Windows; using System.Windows.Media; namespace Petzold.TextGeometryDemo { public class TextGeometry { // Приватные поля, используемые открытыми свойствами string txt = ""; FontFamily fntfam = new FontFamilyO; Fontstyle fntstyle = FontStyles.Norma1;
Классы Geometry и Path 829 FontWeight fntwt = FontWeights.Normal; FontStretch fntstr = FontStretches.Normal: double emsize = 24; Point ptOrigin = new Pointd), 0); // Открытые свойства public string Text { set { txt = value; } get { return txt; } } public FontFamily FontFamily { set { fntfam = value; } get { return fntfam; } } public Fontstyle Fontstyle { set { fntstyle = value; } get { return fntstyle; } } public FontWeight FontWeight { set { fntwt = value; } get { return fntwt; } } public FontStretch FontStretch { set { fntstr = value: } get { return fntstr: } } public double FontSize { set { emsize = value; } get { return emsize; } } public Point Origin { set { ptOrigin = value; } get { return ptOrigin; } } // Открытое свойство, возвращающее объект Geometry public Geometry Geometry { get { FormattedText formtxt = new FormattedText(Text, Cultureinfo.Currentculture, FlowDi recti on.LeftToRi ght,
830 Глава 28. Классы Geometry и Path new Typeface(FontFamily, Fontstyle, FontWeight, FontStretch), FontSize, Brushes.Bl ack); return formtxt.BuildGeometry(Origin): } } // Необходимо для анимации public PathGeometry PathGeometry { get { return PathGeometry.CreateFromGeometry(Geometry): } } } Как видите, класс получился довольно простым. Он не содержит зависимых свойств, но на тот момент мне был нужен объект, который можно было бы ис- пользовать в виде ресурса и задавать его свойства. Это позволило бы использо- вать свойство Geometry как разовый источник при привязке данных. Далее приведен пример использования этого класса. XAML-файл создает два объекта TextGeometry в виде ресурсов. Один объект содержит слово «Hollow», а другой — слово «Shadow». В обоих случаях используется шрифт Times New Roman Bold с размером 144 пункта. TextGeometryWindow.xaml < j-- =========================га^ TextGeometryWindow.xaml (с) 2006 by Charles Petzold <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:src="clr-namespace:Petzold.TextGeometryDemo" Title="TextGeometry Demo"> <Window.Resources> <src:TextGeometry x:Key="txtHollow" Text="Hollow" FontFamily="Times New Roman" FontSize="192" FontWeight="Bold" /> <src:TextGeometry x:Key="txtShadow" Text="Shadow" FontFamily="Times New Roman" FontSize="192" FontWeight="Bold" /> </Window.Resources> <TabControl> <TabItem Header="Hollow"> <Path Stroke="Blue" StrokeThickness="5" Data="{Binding Source={StaticResource txtHollow), Path=Geometry}" /> </Tabltem>
Классы Geometry и Path 831 <TabItem Header="Dotted"> <Path Stroke="Blue" StrokeThickness="5" StrokeDashArray="{Binding Source={x:Static DashStyles.Dot). Path=Dashes}" StrokeDashCap="Round" Data="{Binding Source={StaticResource txtHollow), Path=Geometry}" /> </TabItem> <TabItem Header="Shadow"> <Canvas> <Path Fill="DarkGray" Data="{Binding Source={StaticResource txtShadow}, Path=Geometry}" Canvas.Left="12" Canvas.Top="12" /> <Path Stroke="Black" Fill="White" Data="{Binding Source={StaticResource txtShadow}, Path=Geometry}" /> </Canvas> </TabItem> </TabControl> </Window> Окно содержит элемент TabControl с тремя вкладками для выбора действий, выполняемых со свойством Geometry объектов TextGeometry. Первая вкладка про- сто обводит контур синей кистью. Вроде бы просто, но в результате вы получаете контурные символы, которые, насколько мне известно, нельзя получить в WPF никаким другим способом. Вторая вкладка работает аналогично, но рисует контур пунктирной линией. Третья вкладка выводит текст с имитацией тени. Следующий файл определения приложения завершает проект. TextGeometryApp.xaml <! _ _ ================================================== TextGeometryApp.xaml (с) 2006 by Charles Petzold Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" StartupUri="TextGeometryWindow.xaml" /> При работе с контурным текстом символы должны быть достаточно больши- ми. Помните: это низкоуровневые графические данные, нс содержащие хиптов, поэтому при малых размерах их качество заметно ухудшается.
29 Преобразования В главе 27 я привел пару XAML-файлов, которые отображали один объект Polygon в двух разных режимах FillMode. Вместо того чтобы сохранять два набора коор- динат в двух элементах Polygon, я определил координатные точки только раз в элементе Style. Для отображения фигур в двух разных частях панели Canvas использовались разные значения свойств Canvas.Left. Фактически эти свойства определяли дополнительное смещение координат X и Y у точек многоугольни- ка. В конце главы 28 нечто похожее было сделано для имитации тени, отбрасы- ваемой текстом. Хотя в отдельных случаях бывает удобно использовать свойства Canvas.Left и Canvas.Top, было бы желательно иметь более общий и систематический способ изменения всех координат конкретного графического объекта. Проблема реша- ется при помощи графических преобразований. Возможности преобразований не ограничиваются смещением координат; увеличение и уменьшение фигур, даже повороты — все это можно сделать при помощи преобразований. Преобразования особенно удобны при анимации. Допустим, требуется пере- местить объект Polygon из одного места в другое. Что удобнее анимировать — весь набор координат или только коэффициент преобразования, применяемый ко всей фигуре? Некоторые эффекты (прежде всего связанные с поворотами) достаточно сложно реализовать без использования преобразований. Преобразование можно применить к любому объекту, производному от UIEIe- ment. Класс UIEIement определяет свойство RenderTransform, которому задается объект типа Transform. В классе FrameworkElement тоже определяется собственное свойство типа Transform, которое называется LayoutTransform. Одна из основных целей этой главы — объяснить суть различий между RenderTransform и Layout- Transform и в каких ситуациях должно использоваться то или иное свойство. У свойств RenderTransform и LayoutTransform есть кое-что общее: они относят- ся к одному типу Transform. Абстрактный класс Transform и его потомки показа- ны в следующей иерархии: Object Dispatcherobject (абстрактный) Dependencyobject Freezable (абстрактный)
Преобразования 833 Animatable (абстрактный) General Transform (абстрактный) GeneralTransformGroup Transform (абстрактный) TranslateTransform ScaleTransform SkewTransform RotateTransform MatrixTransform TransformGroup Потомки Transform перечислены в том порядке, в котором они будут рассмат- риваться в этой главе. Четыре класса, производных от Transform, весьма просты в использовании: TranslateTransform, ScaleTransform, SkewTransform и RotateTransform. Как показывает следующий автономный XAML-файл, эти классы предостав- ляют простые средства для изменения местоположения и внешнего вида эле- ментов. TransformedButtons.xaml <!-- ===================================================== TransformedButtons.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:xs"http://schemas.microsoft.com/wi nfx/2006/xaml"> <Button Canvas.Left="50" Canvas.Top="100"> Untransformed </Button> <Button Canvas.Left="200" Canvas.Top="100"> Translated <Button.RenderTransform> TranslateTransform X="-100" Y="150" /> </Button.RenderTransform> </Button> <Button Canvas.Left="350" Canvas.Top=T00"> Scaled <Button.RenderTransform> <ScaleTransform SealeX="1.5" ScaleY="4" /> </Button.RenderTransform> </Button> <Button Canvas.Left="500" Canvas.Top="100"> Skewed <Button.RenderTransform> <SkewTransform AngleY="20" /> </Button.RenderTransform> </Button>
834 Глава 29. Преобразования <Button Canvas.Left="650" Canvas.Top="100"> Rotated <Button.RenderT ransform> <RotateTransform Angle="-30" /> </Button.RenderTransform> </Button> </Canvas> Без преобразований эти пять кнопок располагались бы в одном ряду. У второй кнопки свойству RenderTransform задан объект типа TranslateTransform, который смещает ее вниз и влево. Третья кнопка подвержена преобразованию ScaleTrans- form, которое увеличивает ее ширину в 1,5 раза, а высоту в 4 раза. Четвертая кнопка наклоняется на 20° по вертикали; из прямоугольника она превращается в параллелограмм. Пятая кнопка поворачивается на 30° против часовой стрел- ки. При этом все кнопки в полной мере сохраняют свою функциональность. В программах С#, как и в XAML-файлах, можно задать свойства класса Trans- form, но при этом класс также содержит ряд полезных конструкторов. Их исполь- зование продемонстрировано в следующей программе С#. Программа, созданная на основе следующего файла TransformedButtons.cs, функционально эквивалентна TransformedButtons.xaml. TransformedButtons.cs // // TransformedButtons.cs (с) 2006 by Charles Petzold // using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media; namespace Petzold.TransformedButtons { public class TransformedButtons : Window { [STAThread] public static void MainO { Application app = new Application(): app.Run(new TransformedButtons()): } public TransformedButtons() { Title = "Transformed Buttons": // Создание объекта Canvas как содержимого окна Canvas canv = new CanvasO; Content = canv;
Преобразования 835 // Кнопка без преобразования Button btn = new ButtonO; btn.Content = "Untransformed"; canv.Chi 1dren.Add(btn); Canvas.SetLeft(btn. 50): Canvas.SetTop(btn, 100): // Перемещение btn = new ButtonO: btn.Content = "Translated"; btn.RenderTransform = new TranslateTransformOlOO, 150): canv.ChiIdren.Add(btn); Canvas.SetLeftCbtn, 200); Canvas.SetTop(btn, 100); // Масштабирование btn = new ButtonO: btn.Content = "Scaled"; btn.RenderTransform = new SealeTransformC1.5, 4): canv.Children.Add(btn); Canvas.SetLeftCbtn, 350): Canvas.SetTop(btn, 100): // Сдвиг btn = new ButtonO: btn.Content = "Skewed": btn.RenderTransform = new SkewTransform(0. 20): canv.Chi 1dren.Add(btn); Canvas.SetLeft(btn, 500): Canvas.SetTop(btn, 100); // Поворот btn = new ButtonO: btn.Content = "Rotated"; btn.RenderTransform = new RotateTransform(-30): canv.Children.Add(btn); Canvas.SetLeft(btn, 650): Canvas.SetTop(btn, 100): } } } Если у вас имеется опыт работы с преобразованиями в других средах графи- ческого программирования (включая Win32 и Windows Forms), вы увидите, что реализация преобразований в WPF несколько отличается от них. В других графи- ческих средах преобразования являются свойствами поверхностей, а все нарисо- ванное на поверхности подвергается текущему преобразованию. В WPF преобра- зования являются свойствами самих элементов и фактически трансформируют
836 Глава 29. Преобразования элемент относительно его исходного состояния. Иногда различие незаметно, ино- гда оно существенно. В нашем примере различие сильнее всего проявляется при повороте кнопки. От нас потребовалось лишь указать угол поворота. В традиционной графической среде поворот производился бы относительно начала координат поверхности, то есть точки (0,0) панели Canvas. В WPF поворот производится относительно начала координат самой кнопки, то есть кнопка поворачивается относительно своего левого верхнего угла, а не левого верхнего угла панели. Ранее я уже упоминал о том, что любой класс, производный от Framework- Element, содержит два свойства типа Transform: RenderTransform и LayoutTransform. В обеих приведенных программах использовалось свойство RenderTransform. По- пробуйте в файле с кодом XAML или C# провести глобальную замену Render- Transform на LayoutTransform и посмотрите, к чему это приведет. Вы увидите, что смещаемая кнопка остается на своем исходном месте в точке (200, 100). Повер- нутая кнопка по-прежнему поворачивается, но немного смещается вниз; в резуль- тате она занимает исходную область, обозначенную присоединенными свойства- ми Canvas.Left и Canvas.Top. Вскоре причины этого странного поведения станут очевидны. Давайте поподробнее рассмотрим разные типы преобразований в небольших программах XAML, с которыми можно поэкспериментировать. Начнем с про- стейшего потомка Transform — преобразования смещения (TranslateTransform). TranslateTransform определяет два свойства с именами X и Y. Эти свойства под- держиваются зависимыми свойствами, поэтому они могут использоваться в ани- мации и привязке данных. В следующем автономном XAML-файле на панели StackPanel размещаются два элемента ScrollBar, у которых свойства Minimum заданы равными -300, а свойства Maximum — 1000. Два элемента TextBlock связаны со свойствами Value этих эле- ментов; в них выводятся текущие значения полос прокрутки. Затем на панели StackPanel размещается панель Canvas с двумя линиями, пересекающимися в точ- ке (100, 100) относительно левого верхнего угла Canvas, и элемент Button, пози- ционируемый в точке (100, 100). Ближе к концу файла свойству RenderTransform кнопки задается объект TranslateTransform, свойства X и Y которого привязаны к полосам прокрутки. InteractiveTranslateTransform.xaml <।_ _ ================================================================ InteractiveTranslateTransform.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentati on" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Label Content=,,X" /> <ScrollBar Name="xscroir Orientation=,,Horizontal" Minimum=,,-300" Maximum=,,1000" /> <TextBlock HorizontalAlignment=,,Center" Margin=,,12"
Преобразования 837 Text="{Binding ElementName=xscroll, Path=Value}" /> <Label Content="Y" /> <ScrollBar Name="yscroH" Orientation^'Horizontal" Minimuni="-300" Maximum="1000" /> <TextBlock Horizontal Alignment^'Center" Margin="12" Text="{B1nding ElementName=yscrol1, Path=Value}" /> <Canvas> <Line Xl="100" Yl="0" X2="100" Y2="1000" Stroke="Black"/> <Line Xl="0" Yl="100" X2="1000" Y2="100" Stroke="Black"/> <Button Content="Button" Canvas.Left="100" Canvas.Top="100"> <Button.RenderT ransform> <TranslateTransform X="{Binding ElementName=xscrol1. Path=Value}" Y="{Binding ElementName=yscrol1, Path=Value}" /> </Button.RenderTransform> </Button> </Canvas> </StackPanel> В начале работы программы левый верхний угол Button размещается в точке (100, 100). При выполнении операций с двумя элементами ScrollBar кнопка пере- мещается по панели Canvas. Координаты левого верхнего угла кнопки равны сумме исходных координат со свойствами X и Y объекта TranslateTransform. Удобная математическая запись поможет формализовать этот процесс. Обо- значим исходную позицию кнопки (х, у), а значения свойств X и Y объекта TranslateTransform — dx и dy. Итоговая позиция кнопки (х', //') выражается сле- дующим образом: х' = х + dx; У' = У + dv. Помните, что значения dx и dy могут быть отрицательными. Перемещая вто- рую полосу влево, можно заставить кнопку Button выйти за пределы панели Canvas и даже накрыть элементы ScrollBar! Перейдите к XAML-файлу и замените два вхождения элемента свойства Button.RenderTransform на Button.LayoutTransform. На этот раз объект Button вообще не перемещается, как бы энергично вы ни перемещали полосы прокрутки. Воз- можно, результат вас разочарует, но он лишь подтвердит упомянутый выше урок: объект TranslateTransform, примененный к свойству LayoutTransform, не влияет на местоположение элемента. Класс ScaleTransform определяет четыре свойства ScaleX, ScaleY (по умолча- нию 1), CenterX и CenterY (по умолчанию 0). Эти свойства предоставляют средства увеличения и уменьшения размеров элемента. Файл InteractiveScaleTransform.xaml создает четыре полосы прокрутки для изменения четырех свойств, а элемент ScaleTransform привязывает свои свойства к текущим значениям полос.
838 Глава 29. Преобразования InteractiveScaleTransform.xaml < j _ _ ============================================================ InteractiveScaleTransform.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentati on" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Label Content="ScaleX" /> <ScrollBar Name="xscroll" Orientation="Horizontal" Value="l" Minimum="-20" Maximum="20" /> <TextBlock HorizontalAlignment="Center" Margin="12" Text="{Binding ElementName=xscroll. Path=Value}" /> <Label Content="ScaleY" /> <ScrollBar Name="yscrol1" Orientation="Horizontal" Value="l" Minimum="-20" Maximum="20" /> <TextBlock HorizontalAlignment^'Center" Margin="12" Text="{Binding ElementName=yscroll, Path=Value}" /> <Label Content="CenterX" /> <ScrollBar Name="xcenter" Orientation="Horizontal" Value="0" Minimum="-100" Maximum=" 100" /> <TextBlock HorizontalAlignment="Center" Margin="12" Text="{Binding ElementName=xcenter. Path=Value}" /> <Label Content="CenterY" /> <ScrollBar Name="ycenter" Orientation="Horizontal" Value="0" Minimum="-100" Maximum="100" /> <TextBlock Horizontal Alignment^'Center" Margin="12" Text="{Binding ElementName=ycenter, Path=Value}" /> <Canvas> <Line Xl="100" Yl="0" X2="100" Y2="1000" Stroke="Black"/> <Line Xl="0" Yl="100" X2="1000" Y2="100" Stroke="Black"/> <Button Name="btn" Content^'Button" Canvas.Left="100" Canvas.Top="100"> <Button.RenderT ransform> <ScaleTransform ScaleX="{Binding ElementName=xscroll. Path=Value}" ScaleY="{Binding ElementName=yscroll. Path=Value}" CenterX="{Binding ElementName=xcenter, Path=Value}" CenterY="{Binding ElementName=ycenter. Path=Value}" /> </Button.RenderT ransform> </Button> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding ElementName=btn, Path=ActualWidth}" />
Преобразования 839 <TextBlock Text="&#x00D7;" /> <TextBlock Text="{Bind1ng ElementName=btn, Path=ActualHeight}" /> </StackPanel> </Canvas> </StackPanel> Ближе к концу файла свойства Actualwidth и ActualHeight элемента Button вы- водятся в левом верхнем углу Canvas. Сначала попробуйте поработать с полосами прокрутки ScaleX и ScaleY. Эти полосы прокрутки увеличивают и уменьшают размеры Button с применением масштабных коэффициентов. Например, если значение ScaleX равно 2, ширина кнопки увеличивается вдвое, а со значением 0,5 ширина составляет половину от исходной. Если сделать свойство ScaleX отрицательным, Button вместе с текстом зеркально отражается вокруг вертикальной оси. При отрицательном значении ScaleY кнопка отражается вокруг горизонтальной оси. Обратите внимание: свойства Actualwidth и ActualHeight объекта Button остают- ся неизменными. Также не изменяется свойство RenderSize, используемое мето- дом OnRender для рисования элемента. Преобразования всегда применяются уже после того, как элемент будет прорисован. Если обозначить масштабные коэффициенты ScaleX и ScaleY sv и s,p то форму- лы, описывающие эффект масштабирования, выглядят так: Х= Sx -Х\ У' = -у. Еще раз подчеркну, что координаты (х, у) и (.г', у’) задаются по отношению к левому верхнему углу непреобразованного объекта Button. Точка (0, 0) преоб- разуется в точку (0, 0). Вот почему левый верхний угол кнопки всегда остается в одном месте независимо от значений ScaleX и ScaleY. Возможно, в каких-то случаях это окажется нежелательным — например, если вы хотите удвоить размеры элемента, но при этом сохранить положение его центра. Иначе говоря, элемент должен расширяться не только вниз и вправо, но во всех четырех направлениях. Задача решается свойствами CenterX и CenterY объекта ScaleTransform. Попро- буйте выставить на полосе прокрутки CenterX половину ширины, а на полосе CenterY — половину высоты кнопки. (Вторая причина, по которой в программе выводятся свойства Actualwidth и ActualHeight объекта Button.) Теперь при увели- чении свойств ScaleX и ScaleY кнопка расширяется во всех четырех направлениях. Если обозначить свойства CenterX и CenterY объекта ScaleTransform csx и cs,p пол- ные формулы масштабирования принимают следующий вид: х = sx • х + (csx - 5 V • csx ); y'=s!/-y+ (cs*- Sy-csJ. Если заменить в файле InteractiveScaleTransform.xaml оба вхождения Button.Ren- derTransform на Button.LayoutTransform, вы увидите, что полосы прокрутки ScaleX и ScaleY по-прежнему увеличивают и уменьшают кнопку и даже отражают ее
840 Глава 29. Преобразования вокруг вертикальной или горизонтальной оси. Тем не менее кнопка всегда остает- ся на одном месте. Она всегда располагается внизу и справа от точки (100, 100), где пересекаются две линии. Это особенно заметно для отрицательных масштаб- ных коэффициентов — кнопка отражается, но остается на прежнем месте. Поло- сы прокрутки CenterX и CenterY не влияют на LayoutTransform, потому что эти по- лосы фактически задают смещение, а мы уже выяснили, что для LayoutTransform оно игнорируется. Переходим к преобразованию сдвига. Класс SkewTransform определяет четыре свойства AngleX, AngleY, CenterX и CenterY; все эти свойства по умолчанию рав- ны 0. Свойства AngleX и AngleY определяют углы наклона. Значения в диапазоне от -90° до 90° обеспечивают уникальные результаты, а значения за пределами диапазона просто дублируют их. InteractiveSkewTransform.xaml <!_- =========================================================== InteractiveSkewTransform.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Label Content^'AngleX" /> <ScrollBar Name="xscroH" Orientation="Horizontal" Value="0" Minimum="-90" Maximum="90" /> <TextBlock HorizontalAlignment="Center" Margin="12" Text="{Binding ElementName=xscroll. Path=Value}" /> <Label Content="AngleY" /> <ScrollBar Name="yscroH" Orientation^'Horizontal" Value="0" Minimum="-90" Maximum="90" /> <TextBlock HorlzontalAlignment="Center" Margin="12" Text="{Binding ElementName=yscrol1, Path=Value}" /> <Label Content="CenterX" /> <ScrollBar Name="xcenter" Orientation="Horizontal" Value="0" Minimum="-100" Maximum="100" /> <TextBlock HorlzontalAlignment="Center" Margin="12" Text="{Binding ElementName=xcenter. Path=Value}" /> <Label Content="CenterY" /> <ScrollBar Name="ycenter" Orientation="Horizontal" Value="0" Minimum="-’100" Maximum="100" /> <TextBlock HorlzontalAlignment="Center" Margin="12" Text="{Binding ElementName=ycenter. Path=Value}" /> <Canvas> <Line Xl="100" Yl="0" X2="100" Y2="1000" Stroke="Black"/> <Line Xl="0" Yl="100" X2="1000" Y2="100" Stroke="Black"/>
Преобразования 841 <Button Name="btn" Content="Button" Canvas.Left=" 100" Canvas.Top="100"> <Button.RenderTransform> <SkewTransform AngleX="{Binding ElementName=xscroll, Path=Value}" AngleY="{Binding ElementNanie=yscroll, Path=Value}" CenterX="{Binding ElementName=xcenter, Path=Value}" CenterY="{Binding ElementName=ycenter, Path=Value}" /> </Button.RenderT ransform> </Button> <StackPanel 0rientation="Hor1zontal"> <TextBlock Text="{Binding ElementNanie=btn, Path=ActualWidth}" /> <TextBlock Text="&#x00D7;" /> <TextBlock Text="{Binding ElementNanie=btn, Path=ActualHeight}" /> </StackPanel> </Canvas> </StackPanel> При перемещении бегунка AngleX объект Button сохраняет прежнюю высоту, а верхний край кнопки остается на том же месте, но ее нижний край сдвигается вправо для положительных угол или влево для отрицательных. Это называется горизонтальным сдвигом (или горизонтальным наклоном). Кнопка перестает быть прямоугольником и принимает форму параллелограмма. Верните AngleX значение 0 и переместите бегунок AngleY. Объект Button со- храняет прежнюю ширину, высоту и позицию левого края, но правая сторона кнопки сдвигается вниз для положительных углов или вверх для отрицательных. В обоих случаях левый верхний угол остается закрепленным в точке (100, 100). Если обозначить AngleX и AngleY av и ос7, формулы сдвига принимают следую- щий вид: х = х + tan(av )• у\ у = у + tan(a// )-х. Обратите внимание: х' и у’ зависят как от х, так и от у. Именно этим форму- лы сдвига отличаются от формул смещения и масштабирования. При изменении свойств AngleX и AngleY левый верхний угол кнопки всегда остается в исходном положении. Чтобы изменить это поведение, следует ис- пользовать свойства CenterX и CenterY. Как и аналогичные свойства класса Scale- Transform, эти свойства задают координаты кнопки, которые должны остаться неизменными, но на практике для преобразования сдвига эти свойства работа- ют несколько иначе. Если задать горизонтальному сдвигу (AngleX) значение, от- личное от значения по умолчанию, все точки вида (х, CenterY) не изменяются под действием сдвига. Аналогично, для одного лишь вертикального сдвига ос- таются неизменными все точки (CenterX, у). Для используемых по умолчанию нулевых значений AngleX и AngleY изменение CenterX и CenterY не влияет на по- ложение элемента.
842 Глава 29. Преобразования Если обозначить CenterX и CenterY ckx и cky, то формулы, описывающие эф- фект сдвига, выглядят так: х = х + tan(a v )•(//- ckx); у'= г/+ tan(ay)-(x- cky). Как и в предыдущих случаях, свойство LayoutTransform работает иначе. Заме- ните в файле InteractiveSkewTransform.xaml все вхождения Button.RenderTransform на Button.LayoutTransform. Вероятно, вы уже предположили, что AngleX и AngleY иска- жают изображение, но кнопка всегда остается в той же позиции — внизу и спра- ва от точки (100, 100). Изменение CenterX и CenterY ни на что не влияет, потому что LayoutTransform не реагирует на смещение. Класс RotateTransform определяет уже знакомые свойства CenterX и CenterY, но в отличие от SkewTransform в нем определяется только одно угловое свойство с именем Angle. InteractiveRotateTransform.xaml < j _ _ ============================================================= InteractiveRotateTransform.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentati on" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Label Content="Angle" /> <ScrollBar Name="angle" Orientation="Horizontal" Value="0" Minimum="0" Maximum="360" /> <TextBlock HorizontalAlignment="Center" Margin="12" Text="{Binding ElementName=angle. Path=Value}" /> <Label Content="CenterX" /> <ScrollBar Name="xcenter" Orientation="Horizontal" Value="0" Minimum="-100" Maximum="100" /> <TextBlock HorizontalAlignment="Center" Margin="12" Text="{Binding ElementName=xcenter. Path=Value}" /> <Label Content="CenterY" /> <ScrollBar Name="ycenter" Orientation="Horizontal" Value="0" Minimum—100" Maximum="100" /> <TextBlock HorizontalAlignment="Center" Margin="12" Text="{Binding ElementName=ycenter, Path=Value}" /> <Canvas> <Line Xl="100" Yl="0" X2="100" Y2="1000" Stroke="Black"/> <Line Xl="0" Yl="100" X2="1000" Y2="100" Stroke="Black"/> <Button Name="btn" Content="Button" Canvas.Left="100" Canvas.Top="100">
Преобразования 843 <Button.RenderT ransform> <RotateTransforni Angle="{Binding ElementName=angle, Path=Value}" CenterX="{Binding ElementName=xcenter, Path=Value}" CenterY="{Binding ElementName=ycenter, Path=Value}" /> </Button.RenderTransform> </Button> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding ElementName=btn, Path=ActualWidth}" /> <TextBlock Text="&#x00D7;" /> <TextBlock Text="{Binding ElementName=btn, Path=ActualHeight}" /> </StackPanel> </Canvas> </StackPanel> Я выбрал для бегунка Angle диапазон от 0 до 360°, но отрицательные значе- ния (и другие значения за пределами этого диапазона) тоже вполне допустимы. Значение Angle задает величину поворота по часовой стрелке; по умолчанию ось поворота находится в левом верхнем углу элемента. Свойства CenterX и CenterY обозначают положение оси поворота относительно левого верхнего угла элемента. Задайте им значения, равные половинам шири- ны и высоты элемента, и поворот будет осуществляться вокруг центра элемента. Размеры элемента всегда остаются постоянными. И снова отредактируйте файл и замените все вхождения Button.RenderTransform на Button.LayoutTransform. Кнопка поворачивается, но остается внизу справа от своей исходной позиции (100, 100). Подсистема макетирования WPF по-разному работает со свойствами Render- Transform и LayoutTransform. В случае RenderTransform система берет изображение, прорисованное методом OnRender элемента, применяет преобразование и поме- щает изображение на экран. Если изображение почему-либо наложится на дру- гой элемент (или окажется под ним) — неважно. Элемент отсекается по грани- цам окна приложения, но в остальном может перемещаться произвольно. С другой стороны, любые изменения в свойстве LayoutTransform инициируют новый проход макетирования с вызовами MeasureOverride и ArrangeOverride, что- бы преобразование учитывалось при размещении элемента в макете. Методам MeasureOverride, ArrangeOverride и OnRender не нужно знать о LayoutTransform, но некоторые значения зависят от макета. Например, свойство DesiredSize дочерне- го объекта элемента отражает LayoutTransform, несмотря на то, что преобразова- ние не учитывается методом MeasureOverride дочернего объекта. Различие между RenderTransform и LayoutTransform наиболее очевидно прояв- ляется в панелях StackPanel, в ячейках Grid с автоматическим выбором размера или в UniformGrid. Оно наглядно проявляется в следующем автономном XAML- файле. Программа строит две панели UniformGrid 3x3, каждая из которых содержит 9 элементов Button. Центральной кнопке каждой сетки назначается преобразова- ние RotateTransform со свойством Angle, равным 45°. На верхней панели UniformGrid
844 Глава 29. Преобразования преобразование RotateTransform применяется к свойству RenderTransform кнопки. На нижней панели RotateTransform применяется к свойству LayoutTransform. RenderTransformAndLayoutTransform.xaml <।_ _ ==================================================================== RenderTransformAndLayoutTransform.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentati on" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" TextBlock.FontSize="18pt" > <!-- Секция RenderTransform --> <TextBlock Margin="24"> RenderTransform </TextBlock> <UniformGrid Rows="3" Columns="3"> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button"> <Button.RenderT ransform> <RotateTransform Angle="45" /> </Button.RenderTransform> </Button> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> </UniformGrid> <!-- Секция LayoutTransform --> <TextBlock Margin="24"> LayoutTransform </TextBlock> <UniformGrid Rows="3" Columns="3"> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" > <Button.LayoutTransform> <RotateTransform Angle="45" /> </Button.LayoutTransform> </Button> <Button Content="Button" />
Преобразования 845 <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> </UniformGrid> </StackPanel> На верхней панели повернутая кнопка ничего не делает. Макет панели Uni- formGrid формируется так, как если бы кнопка вообще не поворачивалась. Кноп- ка размещается поверх элементов, созданных до нее, и позади элементов, со- зданных после нее. Однако вся нижняя панель регулируется с учетом размеров, необходимых для размещения в ячейке повернутой кнопки. Запускайте эту программу каждый раз, когда вы забудете о различиях между RenderTransform и LayoutTransform (в главе 30 приведена анимированная версия программы, которая демонстрирует эти различия еще нагляднее). Если элемент преобразуется в контексте построения макета и вы хотите, чтобы преобразование элемента было отражено в макете, вероятно, вам следу- ет использовать LayoutTransform. Если преобразование задается всего один раз, а в дальнейшем не изменяется, выбор между RenderTransform и LayoutTransform не влияет на быстродействие. Но если преобразование анимируется или привязы- вается к данным, которые могут часто изменяться, по соображениям эффектив- ности лучше использовать RenderTransform. Любые смещения, применяемые к LayoutTransform, игнорируются, поэтому при задании LayoutTransform можно не беспокоиться о смещениях, а также о свойст- вах CenterX и CenterY. Увеличенный, сдвинутый или повернутый элемент всегда располагается на месте, выделенном для него во время формирования макета. Вероятно, вы заметили, что свойство RenderTransform в предыдущем XAML- файле заставляет кнопку поворачиваться вокруг левого верхнего угла. Чтобы элемент поворачивался вокруг другой точки (например, центра), необходимо за- дать свойства CenterX и CenterY объекта Transform. Чтобы задать им координаты центра элемента, необходимо знать его размер. Но в общем случае размер зави- сит от макета, формируемого на стадии выполнения программы и потому недос- тупного в коде XAML По этой причине дальновидные разработчики WPF включили в UIEIement свойство с именем RenderTransformOrigin. Оно используется как альтернативное средство задания начала координат для RenderTransform. Координаты задаются в дробном виде относительно размера элемента (данная схема напоминает стан- дартную систему координат для градиентных кистей). Например, чтобы в приведенной XAML-программе кнопка поворачивалась вокруг своего центра, включите для нее атрибут RenderTransformOrigin. В исход- ной версии поворачиваемая кнопка была представлена следующим элементом: <Button Content="Button"> <Button.RenderT ransform> <RotateTransform Angle="45" /> </Button.RenderT ransform> </Button>
846 Глава 29. Преобразования Замените этот элемент следующим: ^Button Content="Button" RenderTransform0rigin="0.5 0.5"> <Button.RenderTransform> <RotateTransform Angle="45" /> </Button.RenderT ransform> </Button> Теперь кнопка поворачивается вокруг центра. Следующая программа исполь- зует свойство RenderTransformOrigin для вывода одной «эталонной» кнопки и четы- рех кнопок, каждая из которых поворачивается вокруг одного из ее углов. RotatedButtons.xaml <।_ _ ================================================= RotatedButtons.xaml (с) 2006 by Charles Petzold <Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Grid.Resources> <Style TargetType="{x:Type Button}"> <Setter Property="FontSize" Value="48" /> <Setter Property="Content" Value="Button" /> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" /> </Style> </Grid.Resources> <Button /> <Button RenderTransform0rig1n="0 0"> <Button.RenderTransform> <RotateTransform Angle="225" /> </Button.RenderTransform> </Button> <Button RenderTransformOrigin="l 0"> <Button.RenderT ransform> <RotateTransform Angle="135" /> </Button.RenderT ransform> </Button> <Button RenderTransformOrigin="l 1"> <Button.RenderT ransform> <RotateTransform Angle="225" /> </Button.RenderT ransform> </Button> <Button RenderTransformOrigin="0 1">
Преобразования 847 <Button.RenderTransform> <RotateTransform Angle="135" /> </Button.RenderT ransform> </Button> </Grid> Для предотвращения многочисленных повторений элемент Style определяет свойства для всех пяти кнопок, включая значения HorizontalAlignment и Vertical- Alignment, позиционирующие кнопки в центре ячейки Grid. Исходная (не повер- нутая) кнопка представлена элементом <Button /> У других элементов Button свойства RenderTransformOrigin и RenderTransform настроены для поворота элемента вокруг одного из углов. Вероятно, при работе с такими элементами, как Line, Path, Polygon и Polyline на панели Canvas, можно задать свойства CenterX и CenterY для преобразования, не полагаясь па RenderTransformOrigin, потому что размер и местоположение этих объектов уже известны. Однако при работе с элементами в явно заданных коор- динатах следует знать, как работают начала координат при повороте. Для примера возьмем следующий элемент: <L1ne Stroke="Black" Xl^lOO" Yl=u50” X2=,,500" Y2="100" /> Если применить преобразование RotateTransform к свойству RenderTransform элемента, по умолчанию свойства CenterX и CenterY будут равны 0. Поворот будет производиться вокруг точки (0, 0) — то есть точки, находящейся на 100 единиц левее и на 50 единиц выше начальной точки линии. Чтобы поворот произво- дился относительно начальной точки линии, задайте свойству CenterX значе- ние 100, а свойству CenterY — значение 50. Если же осью вращения должен стать центр линии, задайте свойство CenterX равным 300, а свойству CenterY — значе- ние 75. Впрочем, если вы намерены использовать RenderTransformOrigin для задания начала координат поворота, необходимо учесть одно обстоятельство. Считается, что элемент занимает прямоугольник, занимающий область от 0 до 500 по гори- зонтали и от 0 до 100 по вертикали. Значение RenderTransformOrigin (0, 0) со- ответствует точке (0, 0), а не координатам крайней левой точки линии, то есть (100, 50). Значение RenderTransformOrigin (1, 0) соответствует точке (500, 0). В следующей программе C# продемонстрированы два способа поворота груп- пы линий. WheelAndSpokes.cs //----------------------------------------------- // WheelAndSpokes.cs (с) 2006 by Charles Petzold //----------------------------------------------- using System; using System.Windows: using System.Windows.Controls:
848 Глава 29. Преобразования using System.Windows.Input; using System.Windows.Media: using System.Windows.Shapes; namespace Petzold.WheelAndSpokes { public class WheelAndSpokes : Window { [STAThread] public static void MainO { Application app = new Application(); app. Run (new WheelAndSpokesO); public WheelAndSpokesO { Title = "wheel and spokes": // Создание панели Canvas для размещения графики Canvas canv = new CanvasO: Content = canv; // Создание объекта Ellipse Ellipse elips = new EllipseO: el ips.Stroke = SystemColors.WindowTextBrush; elips.Width = 200: el ips.Height = 200: canv.Chi 1dren.Add(elips): Canvas.SetLeft(elips, 50); Canvas.SetTop(elips, 50); // Создание объекта Line for (int i = 0; i < 72: i++) { // Рисование горизонтальной линии Line line = new LineO: line.Stroke = SystemColors.WindowTextBrush; line.XI = 150: line.Y1 = 150; 1ine.X2 = 250; line.Y2 = 150; // Поворот вокруг центра эллипса (150, 150). line.RenderTransform = new RotateTransform(5 * i, 150, 150): canv.Children.Add(line): // Создание другого объекта Ellipse elips = new EllipseO:
Преобразования 849 el ips.Stroke = SystemColors.WindowTextBrush: elips.Width = 200; elips.Height = 200: canv.ChiIdren.Add(elips); Canvas.SetLeft(el ips, 300): Canvas.SetTop(elips, 50); // Создание объектов Line for (int i = 0; i < 72; 1++) { // Рисование горизонтальной линии Line line = new Lined; line.Stroke = SystemColors.WindowTextBrush: line.XI = 0: line.Yl = 0; line.X2 = 100: 1ine.Y2 = 0; // Поворот вокруг точки (0. 0). line.RenderTransform = new RotateTransform(5 * i); // Размещение линии в центре эллипса canv.Children.Add(line); Canvas.SetLeft dine. 400): Canvas.SetTop(line, 150): } } } На левом рисунке создаются 72 элемента Line, начинающиеся в точке (150, 150) (центр колеса) и завершающиеся в точке (250, 150). Всем им назначается преобразование RotateTransform. Первый аргумент конструктора RotateTransform за- дает свойство Angle, а два следующих аргумента — координаты CenterX и CenterY (в данном примере это точка (150, 150)). Линии поворачиваются на разные углы вокруг точки (150, 150). На втором рисунке используется другое решение. Каждый элемент Line начи- нается в точке (0, 0) и заканчивается в точке (100, 0). Свойства CenterX и CenterY для RotateTransform не нужны — значения по умолчанию 0 означают, что линия будет поворачиваться вокруг точки (0, 0). Тем не менее для размещения ли- ний по центру второго круга необходимо задать свойства Canvas.SetLeft и Canvas. SetTop. Конечно, программу можно было бы оформить в виде кода XAML, но в этом случае в файл пришлось бы включить 144 элемента Line, а это противоречит моим инстинктам программиста. Объем повторяющегося кода должен быть ми- нимальным, чтобы изменения в программе вносились как можно проще. Собст- венно, именно для этого в языках программирования существуют циклы.
850 Глава 29. Преобразования Следующий класс, производный от Transform, — класс матричных преобразо- ваний MatrixTransform, в котором определяется свойство Matrix типа Matrix. При- мер его использования: <Button Canvas.Left="650" Canvas.Тор="100"> Rotated <Button.RenderTransform> <MatrixTransform> <MatrixTransform.Matr1x> <Matrix Mll="0.866" M12="-0.5" M21=,,0.5” M22="0.866" /> </MatrixTransform.Matrix> </MatrixTransform> </Button.RenderT ransform> </Button> Если вы не являетесь специалистом в области матричной алгебры, вероятно, вы предпочтете более простые классы конкретных преобразований. Последний класс, производный от Transform, называется TransformGroup. Его важнейшее свойство Children представляет собой коллекцию других объектов Trans- form. Предположим, требуется увеличить кнопку до двойного размера, а также повернуть ее на 45°. Разметка выглядит так: <Button.RenderTransform> <TransformGroup> <ScaleTransform ScaleX="2" ScaleY=,,2" /> <RotateTransform Angle="45" /> </TransformGroup> </Button.RenderT ransform> В этом конкретном примере порядок следования ScaleTransform и RotateTransform неважен, но в общем случае он может оказаться существенным. Впрочем, группировка преобразований не является чем-то «одноразовым». Для Angle можно определить привязку данных или анимацию — обновленное зна- чение будет учтено в объединенном преобразовании. Класс Geometry определяет свойство Transform типа Transform, позволяющее применять преобразования непосредственно к геометрическим объектам. Напри- мер, это может быть удобно при использовании объекта Geometry для определе- ния отсечения. Преобразование можно задать даже в том случае, если объект Geometry является частью Path. По сути геометрическое преобразование приме- няется до любых преобразований RenderTransform или LayoutTransform самого объ- екта Path. Так как преобразование Geometry влияет на координаты и размеры, образующие объект Path, оно будет учтено в построенном макете. Следующая программа рисует два прямоугольника рядом друг с другом. Пер- вый — элемент Rectangle с шириной и высотой в 10 единиц и свойством Render- Transform, увеличивающим размеры в 10 раз. Второй — элемент Path, который также рисует объект RectangleGeometry с шириной и высотой 10, и свойством Trans- form, увеличивающим размер в 10 раз.
Преобразования 851 GeometryTransformDemo.xaml <\-- ======================================================== GeometryTransformDemo.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Rectangle Canvas.Left="100" Canvas.Top="100" Stroke="Red" Width="10" Height="10" RenderTransform="10 0 0 10 0 0" /> <Path Canvas.Left="300" Canvas.Top="100" Stroke="Red"> <Path.Data> <Rectang1 eGeometry Rect="0 0 10 10" Transform="10 0 0 10 0 0" /> </Path.Data> </Path> </Canvas> На экране эти два элемента сильно различаются. Красная линия, окружаю- щая элемент Rectangle, находится под воздействием RenderTransform, а ее толщи- на равна 10 единицам. Красная линия, окружающая элемент Path, имеет ширину всего в одну единицу. Преобразование RenderTransform или LayoutTransform, при- мененное к элементу, влияет на все визуальные аспекты элемента, включая тол- щину линий. Преобразование, примененное к геометрическому объекту, влияет только на координаты в геометрических данных, после чего непреобразованная линия рисуется по преобразованным координатам. Как говорилось в предыдущей главе, вы можете определить панель Canvas оп- ределенного размера, которая будет использоваться в качестве поверхности для рисования. Но возможно, вы предпочтете, чтобы начало координат панели, то есть точка (0, 0), находилось не в левом верхнем углу, а в другом месте. Некото- рые графические программисты используют поверхность рисования, у которой начало координат находится в левом нижнем углу, а координата Y увеличивает- ся по направлению к верхнему краю. Такая схема имитирует правую верхнюю четверть традиционной декартовой системы координат. В некоторых приложе- ниях (например, аналоговых часах) прямо-таки напрашивается система коорди- нат с началом в центре. Для реализации альтернативной системы координат следует задать свойство RenderTransform самого объекта Canvas. В следующем автономном файле Canvas- Modes.xaml продемонстрированы четыре разные системы координат Canvas. CanvasModes.xaml <।_ _ ============================================== CanvasModes.xaml (с) 2006 by Charles Petzold <Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Gri d.ColumnDefi ni ti ons>
852 Глава 29. Преобразования <ColumnDef1nition /> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> </G rid.ColumnDefi ni ti ons> <Grid.Resources> <Style TargetType="{x:Type Canvas}"> <Setter Property="Width" Value="100" /> <Setter Property="Height" Value="100" /> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" /> </Style> <Style TargetType="{x:Type Path}"> <Setter Property="FiH" Value="Red" /> <Setter Property="Data"> <Setter.Value> <EllipseGeometry Center="0 0" RadiusX="5" RadiusY="5" /> </Setter.Value> </Setter> </Style> </Grid.Resources> <!-- Обычная система кооррдинат: начало в левом верхнем углу --> <Canvas Grid.Column="0"> <Line Xl^'G" Yl="0" X2="100" Y2=,,100" Stroke=,,Black" /> <Polyline Points="0 0 0 100 100 100 100 0 0 0" Stroke="Blue" /> <Path /> </Canvas> <!-- Начало координат в левом нижнем углу, ось Y направлена вверх --> <Canvas Grid.Colитп="1"> <Canvas.RenderT ransform> <TransformGroup> <ScaleTransform ScaleY^'-l" /> <TranslateTransform Y="100" /> </TransformGroup> </Canvas.RenderTransform> <Line Xl^'O" Yl="0" X2=,,100n Y2=,,100n Stroke=,,Black" /> <Polyline Points="0 0 0 100 100 100 100 0 0 0" Stroke=,,Blue" /> <Path /> </Canvas> <!-- Начало координат в центре, ось Y направлена вниз --> <Canvas Grid.Column="2"> <Canvas.RenderTransform>
Преобразования 853 <TransformGroup> <ScaleTransform ScaleY="l" /> TranslateTransform X="50" Y="50" /> </TransformGroup> </Canvas.RenderT rans form> <Line Xl="0" Yl="0" X2="50" Y2="50" Stroke="Black" /> <Polyline Points="-50 -50 50 -50 50 50 -50 50 -50 -50" Stroke="Blue" /> <Path /> </Canvas> <!-- Декартова система координат. --> <Canvas Grid.Column="3"> <Canvas.RenderT ransform> <TransformGroup> <ScaleTransform ScaleY="-r /> TranslateTransform X="50" Y="50" /> </TransformGroup> </Canvas.RenderTransform> Tine Xl="0" Yl="0" X2="50" Y2="50" Stroke="Black" /> <Polyline Points--"-50 -50 50 -50 50 50 -50 50 -50 -50" Stroke="Blue" /> <Path /> </Canvas> </Grid> Определение стиля для Canvas задает свойства Width и Height равными 100 еди- ницам, а свойства HorizontalAlignment и VerticalAlignment равными Center. Каждая из четырех панелей Canvas находится в центре ячейки панели Grid, состоящей из четырех столбцов. Определение стиля Path включает объект EllipseGeometry, отцентрованный по точке (0, 0) и окрашенный в красный цвет. Таким образом, красная точка все- гда обозначает начало координат поверхности рисования. Объект Polyline обозна- чает контуры координатной системы в виде квадрата со стороной 100 единиц. Для первых двух систем координат, у которых начало находится в углу, контур начинается в точке (0, 0) и проходит до точки (100, 100). От начала координат в точку (100, 100) проводится черная линия. У двух других координатных сис- тем начало координат находится в центре. Квадратный контур начинается в точ- ке (-50, -50) и заканчивается в точке (50, 50). Черная линия проводится из на- чала координат в точку (50, 50). Будьте внимательны: при создании системы координат, у которой ось У на- правлена вверх, текст будет выводиться на Canvas в перевернутом виде! Если вас это не устраивает, компенсируйте эффект применением объекта ScaleTransform к элементу, в котором отображается текст. Также следует помнить, что поворо- ты с положительными углами производятся против часовой стрелки.
854 Глава 29. Преобразования Попробуйте включить в стиль Canvas следующее свойство: <Setter Property="Background" Value="Aqua" /> Возможно, результат приведет вас в замешательство. Для двух последующих преобразований создается впечатление, что поверхность рисования находится за пределами панели Canvas. Угол Canvas всегда является точкой (0, 0), поэтому единственным способом перемещения точки (0, 0) в центр является перемеще- ние Canvas к углу. Позвольте заверить, что это вполне нормально. Поскольку преобразования применяются к свойству RenderTransform объекта Canvas, система макетирования продолжает считать, что Canvas находится в середине ячейки Grid, хотя сам объ- ект Canvas и не прорисовывается в этой области. Если вы хотите использовать одну из альтернативных систем координат, стро- ку с описанием матрицы можно задать прямо атрибуту RenderTransform объекта Canvas. Так, для второго объекта Canvas, у которого начало координат находится в левом нижнему углу, а ось Y направлена вверх, строка имеет вид RenderTransform="l 00-10 100" Если начало координат находится в центре, а ось Y направлена сверху вниз, строка выглядит так: RenderTransform="l 0 0 1 50 50" Для декартовой системы координат: RenderTransform="l 0 0 -1 50 50" Не забывайте, что все приведенные строки предназначены для квадрата со стороной 100 единиц; для других размеров смещения пропорционально увели- чиваются или уменьшаются. В следующей программе, рисующей цветок, исполь- зуется квадратная панель Canvas со стороной 200 единиц внутри объекта Viewbox. Flower.xaml < । _ _ ================-========================= Flower.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Background="White"> <Viewbox> <!-- Квадрат co стороной 200 единиц в декартовых координатах --> Canvas Width="200" Height="200" RenderTransfonn="l 00-1 100 100" > Canvas .Resources> <!-- Стиль предотвращает лишние повторения в лепестках --> <Style TargetType="{x:Type Path}" x:Key="petal"> Cetter Property="Stroke" Value="Black" />
Преобразования 855 <Setter Property="Fill" Value="Red" /> <Setter Property="Data" Value="M 0 0 C 12.5 12.5, 47.5 12.5, 60 0 C 47.5 -12.5, 12.5 -12.5, 0 0 I" /> </Style> </Canvas.Resources> <!-- Зеленый стебель --> <Path Stroke="Green" StrokeThickness="5" Data="M -100 -100 C -100 -50, -50 50, 0 0"> </Path> <!-- Восемь лепестков, многие повернуты --> <Path Stylе="{StaticResource petal}" /> <Path Style="{StaticResource petal}" RenderTransform=".7 -.7 .7 .7 0 0" /> <Path Style="{StaticResource petal}" RenderTransform="0 -1100 0" /> <Path Style="{StaticResource petal}" RenderTransform="-.7 -.7 .7 -.7 0 0" /> <Path Style="{StaticResource petal}" RenderTransform="-l 00-10 0" /> <Path Style="{StaticResource petal}" RenderTransform="-.7 .7 -.7 -.7 0 0" /> <Path Style="{StaticResource petal}" RenderTransform="0 1 -1 0 0 0" /> <Path Style="{StaticResource petal}" RenderTransform=".7 .7 -.7 .7 0 0" /> <!-- Желтый круг в центре приманивает пчел --> <Path Fill="Yellow" Stroke="Black"> <Path.Data> <EllipseGeometry Center="0 0" RadiusX="15" RadiusY="15" /> </Path.Data> </Path> </Canvas> </Viewbox> </Page> Каждый из восьми лепестков строится из двух кривых Безье и поворачива- ется на угол, кратный 45°. Для сокращения объема разметки я использовал
856 Глава 29. Преобразования строки с элементами матриц. Значения 0,7 являются весьма грубым приближе- нием квадратного корня из 2, но работают нормально. Преобразование TranslateTransform часто используется для создания эффекта тени. В предыдущей главе тень была реализована посредством смещения с ис- пользованием присоединенных свойств Canvas. Вариант с применением Translate- Transform к RenderTransform более универсален, потому что его можно применить к панели любого типа. Следующая программа рисует тень, отбрасываемую тек- стом в ячейке панели Grid. TextDropShadow.xaml <!- - =========================================== TextDropShadow.xaml (с) 2006 by Charles Petzold <Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Grid.Resources> <Style TargetType="{x:Type TextBlock}"> <Setter Property=,,Text" Value="Drop-Shadow" /> <Setter Property="FontFamily" Value="Times New Roman Bold" /> <Setter Property="FontSize" Value="96" /> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" /> </Style> </Grid.Resources> <!-- Тень --> <TextBlock 0pacity="0.5" RenderTransform="l 0 0 1 5 5" /> <!-- Основной текст --> <TextBlock /> </Grid> Обратите внимание: описание стиля получилось настолько полным, что объ- ект TextBlock с основным текстом представляется пустым элементом! Фоно- вая тень смещается на пять единиц. Тени часто окрашиваются в серый цвет, но в данном примере эффект достигается применением 50-процентной прозрачно- сти. Самое важное правило состоит в том, чтобы тень размещалась за текстом, а не перед ним. В следующей программе используется аналогичная методика, но с совершен- но иным визуальным эффектом. Программа выводит белый основной текст (вернее, текст цвета SystemColors.WindowBrush) и серый фоновый текст (вернее, текст цвета SystemColors.GrayTextBrush) со смещением в две единицы. EmbossAndEngrave.xaml < I _ _ ============================================= EmbossAndEngrave.xaml (с) 2006 by Charles Petzold
Преобразования 857 <Gr1d xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Grid.Resources> <Style TargetType="{x:Type TextBlock}"> <Setter Property="FontFamily" Value="Times New Roman" /> <Setter Property="FontSize" Value="144" /> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" /> </Style> </Grid.Resources> <Gri d.RowDefi ni ti ons> <RowDefinition /> <RowDefinition /> </G r id.RowDefi ni ti ons> <!-- Текст тени --> <TextBlock Foreground55"{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"> Emboss <TextBlock.RenderT ransform> <TranslateTransform X="2" Y="2" /> </TextBlock.RenderT ransform> </TextBlock> <!-- Основной текст --> <TextBlock Foreground55" {DynamicResource {x:Stati c SystemColors.WindowBrushKey}}"> Emboss </TextBlock> <!-- Текст тени --> <TextBlock Grid.Row="l" Foreground55" {Dynami cResource {x:Static SystemColors.GrayTextBrushKey}}"> Engrave <TextBlock.RenderTransform> <TranslateTransform X="-2" Y="-2" /> </TextBlock.RenderTransform> </TextBlock> <!-- Основной текст --> <TextBlock Grid.Row55"!" Foreground55" {Dynami cResource {x:Static SystemColors.WindowBrushKey}}"> Engrave </TextBlock> </Grid>
858 Глава 29. Преобразования Два эффекта похожи, но во втором примере используется отрицательное смещение. Поскольку мы привыкли к тому, что источники света располагаются наверху, тень внизу и справа от символов воспринимается как отбрасываемая рельефным текстом (строка «Emboss»). Тень внизу слева создает иллюзию «вдав- ленного» текста (строка «Engrave»). Если применить к тени преобразование сдвига, создается впечатление, что текст «стоит» на поверхности. Я попытался добиться этого эффекта в следую- щей программе. EmpiricalTiltedTextShadow.xaml <!_ _ ============================================================= EmpiricalTiltedTextShadow.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Canvas.Resources> <Style TargetType="{x:Type TextBlock}"> <Setter Property="FontFamily" Value="Times New Roman" /> <Setter Property="FontSize" Value="144" /> <Setter Property="Text" Value="Shadow" /> <Setter Property="Canvas.Left" Value="96" /> <Setter Property="Canvas.Top" Value="192" /> </Style> </Canvas.Resources> <!-- Текст тени --> <TextBlock Foreground="DarkGray"> <TextBlock.RenderT ransform> <TransformGroup> <ScaleTransform ScaleY="3" CenterY="100" /> <SkewTransform AngleX="-45" CenterY='T00" /> </TransformGroup> </TextBlock.RenderT ransform> </TextBlock> <!-- Основной текст --> <TextBlock /> </Canvas> XAML-файл применяет к тени преобразования ScaleTransform и SkewTrans- form. Первое увеличивает высоту тени втрое, а второе поворачивает тень на 45°. Но при запуске программы результат оказывается не таким, как вы рассчи- тывали. Тень находится за фоновым текстом, но располагается не там, где нуж- но — необходимо выровнять базовые линии двух текстов (основного и тени). Проблема решается изменением атрибутов CenterY обоих преобразований, ScaleTransform и SkewTransform, которые должны содержать одинаковые величины.
Преобразования 859 Возможно, придется немного поэкспериментировать, чтобы подобрать нужное значение. Тень должна быть выровнена по основному тексту у нижнего края символа h. Правда, отдельные части других символов опускаются ниже базовой линии, так что их тени все равно будут выглядеть немного странно. Экспери- менты показывают, что значение 131 обеспечивает паи лучший результат. Это число соответствует расстоянию в аппаратно-независимых единицах от верха текстовой строки до базовой линии; это координата Y тени, которая не должна подвергаться преобразованиям. Именно по этой причине она была задана свой- ствам CenterY преобразований. Хотя нам удалось подобрать хорошее значение, конечно, оно не подойдет для других размеров шрифтов. Оно даже не подойдет для других семейств шрифтов из-за различий в распределении высоты символов выше и ниже базовой линии. Если бы программа была написана па языке С#, используемое значение можно было бы вычислить по свойству FontSize объекта TextBlock и свойству Baseline класса FontFamily. Свойство Baselint задает расстояние от верха символов до базо- вой линии по отношению к размеру шрифта. Например, для объекта FontFamily, созданного на базе шрифта Times New Roman, свойство Baseline составляет при- близительно 0,91. Умножая его на размер шрифта 144, используемый в програм- ме EmpiricalTiltcdTextShadow.xaml, мы получаем 131. Конечно, значение 0,91 бесполезно для программиста, пишущего код XAML. Значение Baseline необходимо умножить на FontSize, а в XAML пет возможности выполнить умножение. Или все-таки есть? На самом деле мы выполняли операции умножения и сложения в XAML па протяжении всей главы. Именно для этого существуют преобразования, и мы можем использовать их для выполнения любых вычислений в XAML-файлах. Конечно, вычисления будут не такими компактными и четкими, как эквивалент- ный код С#, — но если вы пишете автономную XAML-программу и не хотите использовать C# только для выполнения одной-двух операций, преобразования определенно решат вашу проблему. Для конкретной задачи вычисления смещения от базовой линии сначала не- обходимо определить объект FontFamily в виде ресурса: <FontFam11y x:Key="fntfam,,> Times New Roman </FontFam11y> Также потребуется другой ресурс для размера шрифта: <s:Double x:Key="fnts1ze"> 144 </s:Doublе> Конечно, в файле должно присутствовать объявление пространства имен для System. После определений ресурсов семейства и разхмера шрифта определяется ресурс TransformGroup, содержащий два объекта ScaleTransform: <TransformGroup x:Key="xform"> <Scal eTransform ScaleX="{B1nd1ng Source={StaticResource fntfam}.
860 Глава 29. Преобразования Path=Baseline}" /> <ScaleTransform ScaleX="{StaticResource fntsize}" /> </TransformGroup> Свойство ScaleX первого объекта ScaleTransform привязано к свойству Baseline ресурса fntfam. Второй множитель — просто ресурс fntsize. Произведение нахо- дится в ресурсе xform, но до него еще нужно уметь добраться. Напомню, что класс TransformGroup наследует от Transform свойство Value. Значение этого свой- ства представляет собой объект Matrix, а свойство МИ объекта Matrix будет содер- жать произведение размера шрифта и свойства Baseline. Свойства CenterY объек- тов ScaleTransform и SkewTransform задаются следующим образом: CenterY="{Binding Source={StaticResource xform}, Path=Value.Mil}" Далее приводится полный код программы. TiltedTextShadow.xaml <!- - =================================================== TiltedTextShadow.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentati on" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:s="clr-namespace:System;assembly=mscorli b" > <Canvas.Resources> <FontFamily x:Key="fntfam"> Times New Roman </FontFamily> <s:Double x:Key="fntsize"> 144 </s:Double> <TransformGroup x:Key="xform"> <ScaleTransform ScaleX="{Binding Source={StaticResource fntfam}, Path=Baseline}" /> <ScaleTransform ScaleX="{StaticResource fntsize}" /> </TransformGroup> <Style TargetType="{x:Type TextBlock}"> <Setter Property="FontFamily" Value="{StaticResource fntfam}" /> <Setter Property="FontSize" Value="{StaticResource fntsize}" /> <Setter Property="Text" Value="Shadow" /> <Setter Property="Canvas.Left" Value="96" /> <Setter Property="Canvas.Top" Value="192" /> </Style> </Canvas.Resources> !-- Текст тени -->
Преобразования 861 <TextBlock Foreground="DarkGray"> <TextBlock.RenderTransform> <TransformGroup> <ScaleTransform ScaleY="3" CenterY="{Binding Source={StaticResource xform}, Path=Value.Mll}" /> <SkewTransform AngleX="-45" CenterY="{Bind1ng Source={StaticResource xform}, Path=Value.Mll}"/> </TransformGroup> </TextBlock.RenderT ransform> </TextBlock> <!-- Основной текст --> <TextBlock /> </Canvas> Стиль для объектов TextBlock содержит ссылки на ресурсы семейства и раз- мера шрифта. Если потребуется изменить одно из значений, отредактируйте его в начале секции Resources, и изменения автоматически актуализируются во всем коде (как в настоящей программе!). Наклонная тень, вычисленная с использованием Baseline, выглядит не совсем корректно, если символы выводимого текста содержат нижние выносные элемен- ты. Класс FontFamily также включает свойство LineSpacing, в котором хранится рекомендуемая величина межстрочных интервалов по отношению к размеру шрифта. Как правило, это значение LineSpacing больше 1. В следующей программе для позиционирования тени слова «quirky» используется свойство LineSpacing вместо Baseline. Результат достаточно близок к желаемому, но абсолютной точ- ности этот способ не обеспечивает. TiltedTextShadow2.xaml <! __ ==================================================== Ti 1tedTextShadow2.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:s="clr-namespace:System:assembly=mscorli b" > <Canvas.Resources> <FontFamily x:Key="fntfam"> Times New Roman </FontFamily> <s:Double x:Key="fntsize"> 144 </s:Double> <TransformGroup x:Key="xform">
862 Глава 29. Преобразования <ScaleTransform ScaleX="{Binding Source={StaticResource fntfam}. Path=LineSpacing}" /> <ScaleTransform ScaleX="{StaticResource fntsize}" /> </TransformGroup> <Style TargetType="{x:Type TextBlock}"> <Setter Property="FontFamily" Value="{StaticResource fntfam}" /> <Setter Property="FontSize" Vaiue="{StaticResource fntsize}" /> <Setter Property="Text" Value="quirky" /> <Setter Property="Canvas.Left" Value="96" /> <Setter Property="Canvas.Top" Value="192" /> </Style> </Canvas.Resources> <!-- Текст тени --> <TextBlock Foreground="DarkGray"> <T extBlock.RenderTransform> <TransformGroup> <ScaleTransform ScaleY="2.5" CenterY="{Binding Source={StaticResource xform}, Path=Value.Mll}" /> <SkewTransform AngleX="-45" CenterY="{Binding Source={StaticResource xform}, Path=Value.Mll}"/> </TransformGroup> </TextBlock.RenderT ransform> </TextBlock> <!-- Основной текст --> <TextBlock /> </Canvas> Следующая программа поворачивает 18 объектов TextBlock, содержащих оди- наковый текст, вокруг общей точки. Обратите внимание: RenderTransformOrigin задает значение (0, 0.5), обозначающее середину левой стороны TextBlock. Сам текст дополняется четырьмя пустыми символами, чтобы начальные буквы не на- кладывались друг на друга. RotatedText.cs //-------------------------------------------- // RotatedText.cs (с) 2006 by Charles Petzold //-------------------------------------------- using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input:
Преобразования 863 using System.Windows.Media; namespace Petzold.RotatedText { public class RotatedText : Window { [STAThread] public static void MalnO { Application app = new ApplicationO; app.Run(new RotatedText()): } public RotatedText() { Title = "Rotated Text": // Объект Canvas представляет содержимое окна Canvas canv = new CanvasO; Content = canv; // Вывод 18 элементов TextBlock с поворотом for (Int angle = 0; angle < 360; angle += 20) TextBlock txtblk = new TextBlockO; txtblk.FontFamlly = new FontFamilyC'Arlal"); txtblk.FontSIze = 24; txtblk.Text = " Rotated Text"; txtblk.RenderTransformOrlgln = new Point(0, 0.5); txtblk.RenderTransform = new RotateTransform(angle); canv.Chi 1dren.Add(txtblk); Canvas.SetLeft(txtblk, 200): Canvas.SetTop(txtblk, 200); } } } Программа была написана на C# только потому, что мне не хотелось повто- рять 18 раз элементы, отличающиеся только преобразованием. Мы вернемся к XAML в следующей программе, которая выводит одинаковый текст в четырех разных комбинациях горизонтальных и вертикальных масштабных коэффици- ентов из набора 1 и -1. Как и в примере TiltedTextShadow.xaml, свойство CenterY двух преобразований задается умножением размера шрифта на свойство Baseline объекта FontFamily. ReflectedText.xaml <! -- ================================================ ReflectedText.xaml (с) 2006 by Charles Petzold
864 Глава 29. Преобразования Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.mlcrosoft.com/winfx/2006/xaml" xml ns:s="clr-namespace:System:assembly=mscorl1b"> Canvas. Resources> <FontFam11y x:Key="fntfam"> Times New Roman </FontFamily> <s:Double x:Key="fntsize"> 96 </s:Double> <TransformGroup x:Key="xform"> <ScaleTransform ScaleX="{Binding Source={StaticResource fntfam}, Path=Baseline}" /> CcaleTransform ScaleX="{Stat1cResource fntsize}" /> </TransformGroup> <Style TargetType="{x:Type TextBlock}"> <Setter Property="FontFamily" Vaiue="{StaticResource fntfam}" /> <Setter Property="FontSize" Value="{StaticResource fntsize}" /> <Setter Property="Text" Value="Reflect" /> <Setter Property="Canvas.Left" Value="384" /> <Setter Property="Canvas.Top" Value="48" /> </Style> </Canvas.Resources> <TextBlock /> <TextBlock> <TextBlock.RenderT ransform> <ScaleTransform ScaleX="-l" /> </TextBlock.RenderT ransform> </TextBlock> <TextBlock> <TextBlock.RenderTransform> <ScaleTransform ScaleY="-l" CenterY="{B1nding Source={StaticResource xform}, Path=Value.Mll}" /> </TextBlock.RenderT ransform> </TextBlock> <TextBlock> <TextBlock.RenderTransform> <ScaleTransform ScaleX="-l" ScaleY="-l" CenterY="{B1nding Source={StaticResource xform}, Path=Value.Mll}" />
Преобразования 865 </TextBlock.RenderTransform> </TextBlock> </Canvas> Следующий XAML-файл похож на предыдущий, но в нем отраженный текст поворачивается на 45° с использованием преобразования RotateTransform, кото- рое является частью TransformGroup. RotateAndReflect.xaml <! -- =================================================== RotateAndReflect.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.nricrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" xml ns:s="clr-namespace:System;assembly=mscorli b"> Canvas. Resources> <FontFamily x:Key="fntfam"> Times New Roman </FontFamily> <s:Double x:Key="fntsize"> 96 </s:Double> <TransformGroup x:Key="xform"> <ScaleTransform ScaleX="{Binding Source={StaticResource fntfam), Path=Baseline}" /> <ScaleTransform ScaleX="{StaticResource fntsize}" /> </TransformGroup> <Style TargetType="{x:Type TextBlock}"> <Setter Property="FontFamily" Value="{StaticResource fntfam}" /> <Setter Property="FontSize" Value="{StaticResource fntsize}" /> <Setter Property="Text" Value="Reflect" /> <Setter Property="Canvas.Left" Value="288" /> <Setter Property="Canvas.Top" Value="192" /> </Style> </Canvas.Resources> <TextBlock> <TextBlock.RenderT ransform> <RotateTransform Angle="45" CenterY="{Binding Source={StaticResource xform}, Path=Value.Mll}“ /> </TextBlock.RenderTransform> </TextBlock> <TextBlock> <TextBlock.RenderTransform>
866 Глава 29. Преобразования <TransformGroup> <ScaleTransform ScaleX=,,-l" /> <RotateTransform Angle=,,45" CenterY="{B1nd1ng Source={Stat1cResource xform}, Path=Value.Mll}" /> </TransformGroup> </TextBl ock.RenderT ransform> </TextBlock> <TextBlock> <TextBlock.RenderTransform> <TransformGroup> <ScaleTransform ScaleY=,,-l" CenterY="{B1nding Source={Stat1cResource xform}, Path=Value.Mll}" /> <RotateTransform Angle=,,45" CenterY="{Bind1ng Source={Stat1cResource xform}, Path=Value.Mll}" /> </TransformGroup> </TextBlock.RenderTransform> </TextBlock> <TextBlock> <TextBlock.RenderTransform> <TransformGroup> <ScaleTransform ScaleX="-T ScaleY=,,-l" CenterY="{B1nd1ng Source={StaticResource xform}, Path=Value.MH}" /> <RotateTransform Angle="45" CenterY="{B1nding Source={StaticResource xform}, Path=Value.Mll}" /> </TransformGroup> </TextBlock.RenderT ransform> </TextBlock> </Canvas> Припомните все разнообразие манипуляций с графическими объектами в этой главе. Однако этим дело не ограничивается — ко всем рассмотренными преобра- зованиям можно применить анимацию!
2Q Анимация Допустим, имеется окно программы с кнопкой. Мы хотим, чтобы в момент щелч- ка кнопка увеличивалась в размерах, причем не просто «перепрыгивала» от меньшего размера к большему — увеличение должно происходить плавно. Дру- гими словами, изменение размера кнопки должно быть анимировано. Именно этой теме — средствам анимации Microsoft WPF — посвящена настоящая глава. В одной из начальных глав книги была продемонстрирована методика ани- мации с использованием объекта DispatcherTimer. Организовать постепенное уве- личение кнопки по сигналам таймера несложно. EnlargeButtonWithTimer.cs //------------------------------------------------------- // EnlargeButtonWithTimer.cs (с) 2006 by Charles Petzold // using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: using System.Windows.Thread!ng: namespace Petzold.EnlargeButtonWithTimer { public class EnlargeButtonWithTimer : Window { const double initFontSize = 12: const double maxFontSize = 48: Button btn: [STAThread] public static void MainO { Application app = new Application); app.Run(new EnlargeButtonWithTimer()); } public EnlargeButtonWithTimerO
868 Глава 30. Анимация { Title = "Enlarge Button with Timer"; btn = new ButtonO; btn.Content = "Expanding Button"; btn.FontSize = initFontSize; btn.Horizontal Alignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.Click += ButtonOnClick; Content = btn; } void ButtonOnClick(object sender. RoutedEventArgs args) { DispatcherTimer tmr = new DispatcherTimer(); tmr.Interval = TimeSpan.FromSeconds(O.l); tmr.Tick += TimerOnTick; tmr.Start(); } void Timer0nT1ck(object sender. EventArgs args) { btn.FontSize += 2; if (btn.FontSize >= maxFontSize) { btn.FontSize = initFontSize; (sender as DispatcherTimer).Stop(); } } } } Исходный размер шрифта кнопки равен 12 аппаратно-независимым едини- цам. При щелчке обработчик события создает объект DispatcherTimer, генерирую- щий события Tick каждую десятую долю секунды. Метод TimerOnTick увеличива- ет FontSize на две единицы каждую десятую секунды, пока размер не достигнет 48 единиц, после чего кнопка восстанавливается в исходных размерах, а таймер останавливается. Нетрудно вычислить, что весь процесс займет около 1,8 секун- ды: достаточно умножить интервал срабатывания таймера на разность значений FontSize и разделить на приращение свойства FontSize. Что произойдет, если снова щелкнуть на кнопке во время увеличения? Обра- ботчик события Click создаст второй таймер, поэтому кнопка будет увеличивать- ся вдвое быстрее. Когда один из двух таймеров увеличит ее до 48 единиц, обра- ботчик события Tick восстановит исходный размер кнопки и остановит таймер, однако второй таймер продолжит увеличивать кнопку. Конечно, такое поведе- ние легко предотвратить: установите логический флаг при запуске таймера и не запускайте второй таймер при установленном флаге. WPF обладает парой особенностей, обеспечивающих успех такой анимации. Графическая система спроектирована таким образом, что размеры и позиция визуальных объектов могут изменяться без мерцания или лишних перерисовок.
Анимация 869 Система зависимых свойств гарантирует, что при изменении свойства FontSize кнопка увеличится в соответствии с новым размером шрифта. Если какой-либо способ анимации потребует большого объема кода, вы все- гда можете вернуться к DispatchTimer, однако во многих случаях можно исполь- зовать средства анимации, встроенные в WPF. Большинство классов, связанных с анимацией, определяется в пространстве имен System.Windows.Media.Animation. В анимации WPF всегда используются зависимые свойства. Пространство имен System.Windows.Media.Animation содержит многочисленные классы для ани- мации свойств разных типов. Список анимируемых типов в алфавитном поряд- ке: Boolean, Byte, Char, Color, Decimal, Double, Intl6, Int32, Int64, Matrix, Object, Point, Point3D, Quaternion, Rect, Rotation3D, Single, Size, String, Thickness, Vector и Vector3D. Свойство FontSize относится к типу double, поэтому для анимации свойства FontSize применяется анимация, изменяющая размер зависимого свойства типа double. Свойства типа double очень широко применяются в WPF, поэтому тип double, вероятно, является самым распространенным анимируемым типом. Од- нако в действительности анимация double реализуется тремя классами, представ- ленными в следующей иерархии: Object Dispatcherobject (абстрактный) Dependencyobject Freezable (абстрактный) Animatable (абстрактный) Timeline (абстрактный) AnlmatlonTImelIne (абстрактный) DoubleAnlmatlonBase (абстрактный) DoubleAnimation Doubl eAnlmatlonllsingKeyFrames Doubl eAnlmatlonllsIngPath Для каждого из 22 анимируемых типов существует абстрактный класс, имя которого состоит из типа данных и суффикса AnimationBase — например, Double- AnimationBase. Я буду использовать для таких классов обозначение <77/n>Anima- tionBase. Для всех 22 типов также существуют классы <Twn>AnimationllsingKey- Frame. У всех типов, кроме пяти, существует класс <71wfl>Animation, и только у трех типов имеется класс <TwA?>AnimationUsingPath. Всего существует 64 класса <Тип> AnimationBase и производных от них. Самым тривиальным из потомков DoubleAnimationBase является класс Double- Animation: он просто линейно изменяет свойство типа double от одного значения до другого. В следующей программе объект DoubleAnimation используется для имитации поведения программы EnlargeButtonWithTimer. EnlargeButtonWithAnimation.cs // // EnlargeButtonW1thAn1mat1on.cs (с) 2006 by Charles Petzold // using System; using System.Windows;
870 Глава 30. Анимация using System.Windows.Controls; using System.Windows.Input: using System.Windows.Media; using System.Windows.Media.Animation; namespace Petzold.EnlargeButtonWithAnimation { public class EnlargeButtonWithAnimation : Window { const double initFontSize = 12; const double maxFontSize = 48; Button btn; [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new EnlargeButtonWithAnimation()); } public EnlargeButtonWithAnimationO { Title = "Enlarge Button with Animation"; btn = new ButtonO; btn.Content = "Expanding Button"; btn.FontSize = initFontSize: btn.HorizontalAlignment = HorizontalAlignment.Center; btn.Vertical Alignment = VerticalAlignment.Center; btn.Click += ButtonOnClick: Content = btn; } void ButtonOnClick(object sender, RoutedEventArgs args) { DoubleAnimation anima = new DoubleAnimationO; anima.Duration = new Duration(TimeSpan.FromSeconds(2)): anima.From = initFontSize; anima.To = maxFontSize; anima.FillBehavior = FillBehavior.Stop; btn.BeginAnimation(Button.FontSizeProperty. anima): } } } Конструктор создает объект Button таким же способом, как и в предыдущей программе, но обработчик события Click создает объект типа DoubleAnimation. Каж- дая анимация обладает определенной продолжительностью, которая задается при помощи структуры Duration. В данном примере она задается равной двум секундам. Затем обработчик ButtonOnClick задает свойства From и То объекта DoubleAni- mation, указывая тем самым, что анимация изменяет свойство double от началь- ного значения initFontSize (12) до конечного значения maxFontSize (48). Размер FontSize в любой промежуточный момент анимации определяется методом ли- нейной интерполяции.
Анимация 871 Задавать свойство From в данном примере не нужно, потому что исходный размер FontSize уже равен 12. Но если в свойстве From указано значение, отлич- ное от уже заданного для анимируемого элемента, анимация начнется с перехо- да к этому значению. Свойство FillBehavior указывает, что должно произойти, когда double достиг- нет своего значения 48 и анимация завершится. По умолчанию используется значение FillBehavior.HoldEnd; оно означает, что значение свойства остается рав- ным 48. В режиме FillBehavior.Stop свойство double возвращается к исходному значению до начала анимации. Метод BeginAnimation определяется классом UIEIement (и еще некоторыми классами). Конечно, он наследуется классом Button. Первый аргумент содержит анимируемое зависимое свойство, а второй — объект DoubleAnimation, используе- мый для анимации этого свойства. Вместо метода BeginAnimation объекта Button также можно вызвать метод BeginAnimation окна: Beg1nAn1 mat1 on(Window.FontSizeProperty. anima); Анимация изменит значение FontSize объекта Window, а объект Button унасле- дует анимируемое значение через стандартный механизм наследования свойств вместе с остальными дочерними элементами. В схеме зависимых свойств анима- ция обладает максимальным приоритетом при задании свойства. Вместо BeginAnimation программа также может инициировать анимацию вы- зовом ApplyAnimationClock — другого метода, определяемого классом UIEIement: btn.Appl yAni mat i ondock(Button.FontSizeProperty. anima.Createdock()); Второй аргумент представляет собой объект типа Animationclock, возвращае- мый методом CreateClock, который определяется классом AnimationTimeline и на- следуется классом DoubleAnimation. Объект типа Timeline представляет период времени. В этом классе определя- ются свойства Duration и FillBehavior, а также другие свойства, о которых мы пого- ворим ниже. Класс AnimationTimeline, производный от Timeline, определяет метод CreateClock. Он создает объект Animationclock — механизм, задающий темп анимации. Сам класс DoubleAnimation определяет свойства From и То, относящиеся к типу double. В двух программах С#, приводившихся ранее, анимация инициировалась со- бытием Click объекта Button. Однако анимация может запускаться и по другим условиям. Например, можно установить таймер, который проверяет время су- ток и запускает анимацию в полночь. Или программа может наблюдать за фай- ловой системой при помощи класса FileSystemWatcher и запускать анимацию при создании нового каталога. Или анимация может запускаться на основании дан- ных, получаемых из второго программного потока... и т. д. С другой стороны, анимации, определяемые в XAML, всегда связываются с триггерами. Мы познакомились с триггерами и коллекциями триггеров в гла- вах 24 и 25. В каждом из классов Style, ControlTemplate и DataTemplate определяется свойство Triggers, содержащее коллекцию объектов TriggerBase. В главе 24 были описаны четыре класса, производных от TriggerBase: Trigger, MultiTrigger, DataTrigger
872 Глава 30. Анимация и MultiDataTrigger. Напомню, что триггеры изменяют свойства элементов на осно- вании изменений в других свойствах или привязках данных. В главе 24 не рассматривался пятый класс, производный от TriggerBase, — EventTrigger, так как этот вид триггеров используется в основном при анимации. Именно этот класс обычно применяется в XAML, хотя анимации также могут запускаться триггерами других типов. Возможно, вы уже успели самостоятельно обнаружить, что класс Framework- Element определяет свойство Triggers, содержащее коллекцию объектов Trigger. Эта коллекция Triggers не рассматривалась в предыдущих главах, потому что она весьма специфична. В коллекции Triggers, определяемой FrameworkElement, хра- нятся только объекты EventTrigger, а с этими объектами трудно сделать что-то большее, чем запуск анимации или воспроизведение звуков. Класс EventTrigger определяет три свойства. Свойство SourceName содержит строку со ссылкой на атрибут Name или x:Name элемента, инициирующего ани- мацию. (На практике свойство SourceName часто косвенно определяется контек- стом EventTrigger и задавать его явно не обязательно.) Свойство RoutedEvent клас- са EventTrigger задает конкретное инициирующее событие. В классе EventTrigger также определяется свойство Actions, которое указывает, какие действия должны выполняться при возникновении события. Свойство Actions представляет собой коллекцию объектов TriggerAction; среди классов, производных от TriggerAction, важнейшим является класс BeginStoryboard. Секция Triggers элемента Button выглядит примерно так: <Button.Triggers> <ЕventTr1gger RoutedEvent="Button.Cli ck"> <BeginStoryboard ... > </BeginStoryboard> </EventTrigger> </Button.Triggers> В XAML-анимации всегда задействованы кадровые планы (storyboards) — объ- екты, дополняющие Timeline информацией о целевых свойствах. (Кадровые пла- ны также можно использовать в программном коде, но, как вы уже видели, это не обязательно.) Дочерним элементом BeginStoryboard часто является элемент Story- board. Как и DoubleAnimation, класс Storyboard является производным от Timeline: Object Dispatcherobject (абстрактный) Dependencyobject Freezable (абстрактный) Animatable (абстрактный) Timeline (абстрактный) AnimationTimeline (абстрактный) DoubleAnimationBase (абстрактный) DoubleAnimation DoubleAnimationUsingKeyFrames DoubleAni mati onUsi ngPath
Анимация 873 Time]IneGroup (абстрактный) ParallelTimeline Storyboard MediaTimeline Многоточием обозначены различные классы анимации для всех 22 анимируе- мых типов данных. Класс Storyboard также имеет присоединенные свойства для обозначения имени и свойства элемента, к которому применяется анимация (иногда имя эле- мента косвенно определяется контекстом). Класс TimelineGroup определяет свой- ство Children для коллекции временных диаграмм, чтобы элемент DoubleAnimation мог быть дочерним по отношению к Storyboard. Вот как выглядит типичный фрагмент разметки вплоть до объекта DoubleAnimation: <Button.Triggers> <EventTri gger RoutedEvent="Button.Cli ck"> <BeginStoryboard ... > <Storyboard ... > <DoubleAnimation ... /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> Может показаться, что количество промежуточных уровней разметки до Double- Animation слишком велико, но ничего лишнего здесь нет. Секция ButtonTriggers может содержать несколько элементов EventTrigger. В качестве действия тригге- ра часто используется объект BeginStoryboard, но им также может быть объект SoundPlayerAction или ControllableStoryboardAction. BeginStoryboard всегда имеет до- черний элемент Storyboard, а последний может иметь несколько дочерних эле- ментов анимаций. Далее приводится XAML-аналог анимации, увеличивающей кнопку при щелчке. EnlargeButtonlnXaml.xaml <।_ _ ================================================ EnlargeButtonlnXaml.xaml (с) 2006 by Charles Petzold <Page xml ns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Button FontSize=,,12" HorizontalAlignment="Center" VerticalAlignment="Center"> Expanding Button <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard> <Storyboa rd Ta rgetProperty="FontSi ze">
874 Глава 30. Анимация <DoubleAnimation From="12" То="48" Duration="0:0:2" Fl 11 Behavior=,,Stop" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button> </Page> Секция Triggers является частью элемента Button, поэтому источником для EventTrigger становится сам объект Button, а инициирующим событием — Click. В элементе Storyboard указано целевое свойство анимации Targetproperty. Аними- руемое свойство должно быть зависимым свойством. Впрочем, Targetproperty — нечто большее, чем просто свойство Storyboard. Это присоединенное свойство Storyboard, которое может быть вынесено в элемент DoubleAnimation, где оно представляется в виде Storyboard.TargetProperty: <Storyboard> <DoubleAnimation Storyboard.TargetProperty="FontSize" ... /> </Storyboard> Storyboard также определяет присоединенное свойство TargetName, чтобы раз- ные анимации могли ссылаться на разные элементы XAML-файла. Присоединен- ные свойства Storyboard.TargetName и Storyboard.TargetProperty обычно определяют- ся в дочерних элементах Storyboard, но если все дочерние анимации в Storyboard обладают одинаковыми атрибутами TargetName или Targetproperty, атрибут можно передать в элемент Storyboard, как это сделано в файле EnlargeButtonlnXaml.xaml. В следующей разновидности предыдущего XAML-файла используются две кнопки на панели StackPanel. Им присвоены атрибуты Name со значениями «btnl» и «btn2». EnlargeButtonsInXaml.xaml <I_ _ ================================================= EnlargeButtonsInXaml.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <StackPanel HorizontalAlignment=,,Center" Vertical Al ignment="Center"> <Button Name=,,btnl" FontSize="12" Margin="12" HorlzontalAlignment="Center"> Expand Other Button </Button> <Button Name="btn2" FontSize="12" Margin="12"
Анимация 875 Hori zontalAl 1gnment="Center"> Expand Other Button </Button> </StackPanel> <Page.Triggers> <EventTrigger SourceName="btnr RoutedEvent=,,Button.Click"> <BeginStoryboard> <Storyboard> <DoubleAni mat i on Storyboa rd.Ta rgetName="btn2" Storyboa rd.Ta rgetProperty="FontSi ze" From="12" To=,,48" Duration=,,0:0:2" FillBehavior=,,Stop" /> </Storyboard> </BeginStoryboard> </EventTrigger> <EventTrigger SourceName="btn2" RoutedEvent=,,Button.Click"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetName="btnr Storyboard.TargetProperty="FontSize" From="12" To="48" Duration="0:0:2" FillBehavior="Stop" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Page.Triggers> </Page> Два элемента EventTrigger находятся в секции Triggers элемента Page, а не в сек- ции Triggers элемента Button. Поскольку элементы EventTrigger в действительности относятся к событиям Button, они должны содержать атрибут SourceName, кото- рый обозначает элемент, генерирующий событие. Каждый объект DoubleAnimation также содержит атрибут Storyboard.TargetName, определяющий анимируемый эле- мент управления. В ранних версиях этой программы щелчок на кнопке приво- дил к ее увеличению, но я решил, что будет интереснее переключить цели, что- бы при щелчке увеличивалась другая кнопка. Анимация происходит за промежуток времени, обозначенный атрибутом Duration; обычно он задается в формате часы'минутьг.секунды. Допускаются дроб- ные значения секунд. Например, продолжительность в 2,5 секунды задается сле- дующим образом: Duration="0:0:2.5" Если атрибут Duration не указан или содержит строку «Automatic», анимация происходит в течение одной секунды. (В других контекстах атрибуту Duration можно задать строку «Forever», но в этом конкретном контексте данный вари- ант не работает.)
876 Глава 30. Анимация В двух приведенных XAML-анимациях свойство FontSize объекта Button из- меняется от начального до конечного значения, заданных атрибутами From и То элемента DoubleAnimation. Если опустить атрибут From, анимация будет работать так же, потому что атрибут FontSize объекта Button изначально равен 12. Вместо То может использоваться атрибут By: Ву="100" В этом случае конечное значение равно сумме начального значения и By. Ат- рибуты То и By являются взаимоисключающими; если в элементе присутствуют оба атрибута, By игнорируется. В предшествующих примерах атрибут FontSize возвращался к начальному значению после завершения анимации. Это происходило в результате задания свойству FillBehavior значения FillBehavior.Stop. По умолчанию используется зна- чение FillBehavior.HoldEnd, при котором FontSize сохраняет конечное значение, на- значенное в процессе анимации. Свойство AutoReverse объекта DoubleAnimation позволяет автоматически запус- тить обратную анимацию после ее воспроизведения: AutoReverse="True" В результате размер FontSize сначала увеличится от 12 до 48 за две секунды, а затем уменьшится с 48 до 12 за следующие две секунды. Атрибут Duration зада- ет время только прямого воспроизведения Timeline. Для многократного воспроизведения анимации следует задать атрибут Repeat- Behavior (его значение может быть дробной величиной): RepeatBehavior="3x" Также RepeatBehavior может определять промежуток времени: RepeatBehavi ог="0:0:10" Если атрибут AutoReverse равен false, анимация изменит FontSize от 12 до 48 пять раз. При истинном атрибуте AutoReverse анимация дважды изменит Font- Size от 12 до 48 и снова до 12, а потом от 12 до 48. Продолжительность, заданная в RepeatBehavior, может быть меньше атрибута Duration. Наконец, при значении "Forever" анимация будет воспроизводиться неограни- ченно долго: RepeatBehavior="Forever" Если атрибуту RepeatBehavior задано значение Forever, значение атрибута Fill- Behavior игнорируется. Если атрибут RepeatBehavior воспроизводит анимацию более одного раза, так- же можно задать атрибут IsCumulative равным true, чтобы при каждой итерации происходило накопление значений. Для примера возьмем следующую разметку: <DoubleAnimation From="12" То="24" Duration="0:0:2" AutoReverse="true"
Анимация 877 RepeatBehavior="3x" IsCumulative="true" /> Первая итерация изменяет FontSize от 12 до 24 и обратно до 12. При второй итерации свойство изменяется от 24 до 36 и обратно до 24, а при третьей — от 36 до 48 и обратно до 36. Чтобы организовать задержку между событием, запускающем анимацию, и на- чалом самой анимации, следует задать атрибут BeginTime: BeginTime=,,0:0:2" В этом примере анимация начнется с 2-секундной задержкой. В дальнейшем BeginTime не влияет на повторения и обратную анимацию. Если значение Begin- Time отрицательно, анимация начинается позже ее реального начала. Например, если в нашем примере задать BeginTime равным —0:0:1, анимация FontSize начнет- ся со значения 30. Атрибут CutoffTime завершает анимацию в заданный момент времени. Так как анимация может продолжаться дольше (или короче) времени, за- данного в свойстве Duration, я буду использовать термин «итоговая продол- жительность» для обозначения продолжительности, полученной под влиянием BeginTime, CutoffTime, RepeatBehavior и AutoReverse. Класс Storyboard является производным от класса TimelineGroup, в котором оп- ределяется свойство Children типа Timeline. Это означает, что Storyboard может управлять несколькими анимациями. Все анимации, использующие один объект Storyboard, инициируются одним и тем же событием. В следующем автономном XAML-файле класс Ellipse используется для рисова- ния шара, находящегося в левом верхнем углу панели Canvas. Элемент EventTrigger активизируется событием MouseDown объекта Ellipse. Объект Storyboard содержит два объекта DoubleAnimation. Первая анимация изменяет присоединенное свойство Canvas.Left, чтобы шар перемещался по горизонтали. Вторая анимация изменяет присоединенное свойство Canvas.Top, обеспечивая вертикальное перемещение шара. Обратите внимание: оба элемента DoubleAnimation включают атрибуты при- соединенного свойства Storyboard.TargetProperty, а целевыми свойствами этих двух анимаций являются сами присоединенные свойства (Canvas.Left и Canvas.Top), поэтому они должны быть заключены в круглые скобки. TwoAnimations.xaml <।_ _ ================================================ TwoAnimations.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <E11 ipse Width="48" Height="48" Fill="Red" Canvas.Left="O" Canvas.Top=,,0"> <Ellipse.Triggers> <EventTrigger RoutedEvent=,,Ellipse.MouseDown"> <BeginStoryboard>
878 Глава 30. Анимация <Storyboard> <DoubleAnimation Storyboa rd.Ta rgetProperty="(Canvas.Left)" From=,,0" To="288" Duration=,,0:0:l" AutoReverse="True" /> <DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" From="0" To="480" Duration="0:0:5" AutoReverse=,,True" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Ellipse.Triggers> </Е11i pse> </Canvas> Анимация запускается щелчком на эллипсе. Продолжительность движения по горизонтали составляет одну секунду, а у движения по вертикали — 5 секунд. У обеих анимаций атрибут AutoReverse задан равным true. Итоговая продолжи- тельность первой анимации равна двум секундам, а второй — 10 секундам. Две анимации начинаются одновременно. Итоговая продолжительность составной анимации равна максимальной итоговой продолжительности всех составляющих, то есть 10 секундам. Шар начинает двигаться вниз и вправо. Через одну секунду он перестает двигаться вправо и начинает двигаться влево. При достижении горизонтальной позиции 0 первая анимация завершается. Вторая анимация продолжает переме- щать шар вниз, а затем возвращает его вверх. Если вы хотите, чтобы две анимации имели разные настройки Duration, но за- вершались одновременно, задайте свойство RepeatBehavior более короткой ани- мации. Следующая программа похожа на предыдущую, но в ней у горизонталь- ного перемещения значение Duration составляет 1/4 секунды. У вертикального движения оно равно 5 секундам. У обеих анимаций атрибут AutoReverse равен true. Чтобы обе анимации завершались одновременно, атрибут RepeatBehavior пер- вой анимации должен быть равен 20х. WiggleWaggle.xaml <!- - =============================================== WiggleWaggle.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml /presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml "> <E11 ipse Width=,,48" Height="48" Fill^Red" Canvas.Left="O" Canvas.Top="0"> <Ellipse.Triggers> <EventTrigger RoutedEvent="El 1i pse.MouseDown"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)"
Анимация 879 From="0" То="288" Duration="0:0:0.25“ AutoReverse="True" RepeatBehavior=“20x“ /> <DoubleAnimation Storyboard.TargetProperty="(Canvas.Top)" From="0" To="480" Duration="0:0:5" AutoReverse="True" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Е11ipse.Triggers> </Е11ipse> </Canvas> Возможно и другое решение — задать атрибуту RepeatBehavior первой ани- мации промежуток времени. Так как вторая анимация выполняется в течение 10 секунд, атрибут будет выглядеть так: RepeatBehavi ог="0:0:10" Так или иначе, после завершения 20 перемещений влево-вправо и одного пере- мещения вверх-вниз, анимация останавливается. Чтобы эта серия перемещений повторилась трижды, задайте атрибут RepeatBehavior первой анимации равным 60х, а атрибут RepeatBehavior второй анимации равным Зх или включите атрибут Re- peatBehavior прямо в элемент Storyboard для обоих элементов DoubleAnimation: <Storyboard RepeatBehavior="3x"> Если вам захочется ускорить анимацию на 50 %, не обязательно изменять значения Duration в отдельных элементах DoubleAnimation — достаточно задать свойство SpeedRatio для Storyboard: <Storyboard SpeedRatio="1.5"> Эта возможность идеально подходит для отладки анимаций, особенно парал- лельных. Начальные значения Duration задаются так, чтобы анимации выполня- лись очень медленно, после чего скорость постепенно повышается установкой SpeedRatio для составной анимации. Все анимации одного уровня начинаются одновременно. Если это нежелатель- но, воспользуйтесь свойством BeginTime и определите задержку начала анимации. Иногда существует группа параллельных анимаций, после завершения которой должна запускаться другая группа параллельных анимаций. Используйте свой- ство BeginTime для всех анимаций второй группы, чтобы они следовали за завер- шением первой группы, или заключите вторую группу в элемент ParallelTimeline и назначьте BeginTime только для этого элемента. Пример: WiggleWaggleAndExplode.xaml <।_ _ ========================================================= WiggleWaggleAndExplode.xaml (с) 2006 by Charles Petzold
880 Глава 30. Анимация <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Ellipse Width="48" Height="48" Fill=,,Red" Canvas.Left=,,O" Canvas.Top="0"> <Ellipse.Triggers> <EventTrigger RoutedEvent="El 1i pse.MouseDown"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" From="0" To="288" Duration=,,0:0:0.25" AutoReverse="True" RepeatBehavior=,,20x" /> <DoubleAnimation Storyboard.TargetProperty=,,(Canvas.Top)" From="0" To="480" Duration=,,0:0:5" AutoReverse=,,True" /> <Para11 elTi meli ne Begi nTi me="0:0:10" FillBehavior=,,Stop"> <DoubleAnimation Storyboa rd.Ta rgetProperty=" Wi dth" From="48" To="480" Duration=,,0:0:l" /> <DoubleAnimation Storyboa rd.Ta rgetProperty="Hei ght" From=,,48" To="480" Duration="0:0:1" /> </Paral1 elTimeline> </Storyboard> </BeginStoryboard> </EventTrigger> </Ellipse.Triggers> </Е11i pse> </Canvas> После возвращения шара в исходную позицию два элемента DoubleAnimation быстро увеличивают его ширину и высоту до 5 дюймов, после чего шар возвра- щается к исходному размеру. Два объекта DoubleAnimation, увеличивающие шар, сгруппированы в элемент ParallelTimeline. У этого элемента атрибут BeginTime равен 10 секундам (итоговая продолжительность первых двух анимаций). Кроме того, элемент ParallelTimeline задает атрибут FillBehavior равным Stop. И BeginTime, и FillBehavior применяются к двум потомкам ParallelTimeline. Эти два свойства можно было бы включить по отдельности в двух потомках DoubleAnimation, но их консолидация в элементе ParallelTimeline упрощает понимание и модификацию программы. В большинстве представленных программ элемент управления (или интер- фейсный элемент) изменялся в ответ на событие данного элемента. Впрочем, события элемента также могут инициировать анимацию в других элементах. Следующий автономный XAML-файл отображает страницу с тремя переключа- телями. Щелчок на любом из них изменяет фоновый цвет страницы.
Анимация 881 В программе используются элементы ColorAnimation, которые похожи на Double- Animation, но их свойства From, То и By относятся к типу Color, а свойство Target- Property тоже должно относиться к типу Color. ColorRadioButtons.xaml <! - - ==================================================== ColorRadioButtons.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Background^'{x:Static SystemColors.WindowBrush}" Name="page"> <Page.Resources> <Style TargetType="{x:Type RadioButton}"> <Setter Property="Margin" Value="6" /> </Style> </Page.Resources> <StackPanel HorizontalAlignment="Center" Vertical Alignment^'Center" Background^'{DynamicResource {x:Static SystemColors.ControlBrushKey}}"> <RadioButton Content="Red"> < Radi oButton.T ri ggers> <EventTrigger RoutedEvent="RadioButton.Checked"> <BeginStoryboard> <Storyboard> ColorAnimation Storyboa rd.Ta rgetName="page" Storyboard.TargetProperty="Background.Col or" To="Red" Duration="0:0:l" /> </Storyboard> </BeginStoryboard> </EventTrigger> < /Rad ioButton.T ri ggers> </RadioButton> <RadioButton Content="Green"> < Radi oButton.Tri ggers> <EventTrigger RoutedEvent="RadioButton.Checked"> <BeginStoryboard> <Storyboard> ColorAnimation Storyboard.TargetName="page" Storyboard.TargetProperty="Background.Col or" To="Green" Duration="0:0:l" /> </Storyboard> </BeginStoryboard> </EventTrigger>
882 Глава 30. Анимация < /Rad1oButton.Т ri ggers> </RadioButton> <RadioButton Content="Blue"> < RadioButton.Triggers> <EventTrigger RoutedEvent="RadioButton.Checked"> <BeginStoryboard> <Storyboard> <ColorAnimation Storyboard.TargetName="page" Storyboard.TargetProperty="Background.Col or" To="Blue" Duration="0:0:1" /> </Storyboard> </BeginStoryboard> </EventTrigger> < /Ra d ioButton.Tri ggers> </RadioButton> </StackPanel> </Page> Свойству Name корневого элемента Page задается значение "page", на которое ссылается каждый из элементов ColorAnimation. Атрибут Targetproperty ссылается на свойство Color свойства Background объекта Раде; предполагается, что свойство Background содержит объект SolidColorBrush, что в общем случае не соответствует действительности. Чтобы этот фрагмент разметки работал, страница должна ини- циализироваться объектом SolidColorBrush, как это сделано в корневом элементе. Анимации не имеют атрибута From, поэтому цвет фона изменяется от текущего до цвета, заданного атрибутом То, за период в одну секунду. Большой объем повторяющейся разметки наводит на мысль, что такие пере- ключатели лучше реализовать на программном уровне. Почему я не воспользо- вался элементом Style для сокращения дубликатов? Конечно, анимации можно включить в секцию Triggers определения Style, но целью анимаций в Style не мо- гут быть другие элементы, кроме того, к которому применяется стиль. Следую- щий автономный XAML-файл включает определение Style для элементов Button. Стиль включает пару элементов Setter, а также два элемента EventTrigger в секции Triggers. Первый триггер инициируется событием MouseEnter, а второй — событи- ем MouseLeave. В обоих случаях запускается анимация DoubleAnimation, изменяю- щая атрибут FontSize объекта Button. FishEyeButtonsl.xaml <I_ _ ================================================== FishEyeButtonsl.xaml (с) 2006 by Charles Petzold <StackPanel xml ns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <StackPanel.Resources> <Style TargetType="{x:Type Button}">
Анимация 883 <Setter Property^'HorizontalAlignment" Value=,,Center" /> <Setter Property="FontSize" Value=,T2" /> <Style.Triggers> <EventTrigger RoutedEvent="Button.MouseEnter"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="FontSi ze" To="36" Duration="0:0:1" /> </Storyboard> </BeginStoryboard> </EventTrigger> <EventTrigger RoutedEvent=,,Button.MouseLeave"> <BeginStoryboard> <Storyboard> <DoubleAnimat ion Storyboard.TargetProperty="FontSi ze" To='T2" Duration="0:0:0.25" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Style.Triggers> </Style> </StackPanel.Resources> <Button>Button <Button>Button <Button>Button <Button>Button <Button>Button <Button>Button <Button>Button <Button>Button <Button>Button </StackPanel> No. 1</Button> No. 2</Button> No. 3</Button> No. 4</Button> No. 5</Button> No. 6</Button> No. 7</Button> No. 8</Button> No. 9</Button> Благодаря двум элементам EventTrigger кнопки имитируют эффект «рыбьего глаза» при прохождении над ними указателя мыши. Когда указатель мыши за- держивается над кнопкой, последняя постепенно увеличивается втрое, а при от- воде указателя кнопка быстро возвращается к обычному размеру. Если навести указатель на кнопку, а затем быстро отвести его обратно, кнопка не будет увеличена до полного размера 36, указанного в первом элементе Double- Animation. Такое поведение является результатом свойства HandoffBehavior, опре- деленного в BeginStoryboard. Данное свойство управляет взаимодействием разных анимаций, влияющих на одно свойство. По умолчанию свойство HandoffBehavior содержит значение HandoffBehavior.SnapshotAndReplace, при котором предыдущая анимация останавливается с фиксацией текущего значения, после чего заменя- ется новой анимацией.
884 Глава 30. Анимация Чтобы увидеть, как работает альтернативное решение, измените элемент BeginStoryboard в секции MouseLeave и задайте атрибуту HandoffBehavior значение HandoffBehavior.Compose: <BeginStoryboard HandoffBehavior="Compose"> Также задайте атрибут Duration равным одной секунде. Если теперь навести указатель мыши на кнопку и быстро отвести его, кнопка продолжит увеличи- ваться перед тем, как вступит в действие вторая анимация. Анимации, определяемые в элементах, должны инициироваться записью Event- Trigger в секции Triggers этого элемента. Однако в элементе Style анимация не обя- зана инициироваться EventTrigger. В следующем далее альтернативном решени анимация инициируется обычным триггером для свойства MouseOver. FishEyeButtons2.xaml <i__ ================================================== FishEyeButtons2.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentati on" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <StackPanel.Resources> <Style TargetType="{x:Type Button}"> <Setter Property=,,HorizontalAlignment" Value="Center" /> <Setter Property="FontSize" Value="12" /> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <T ri gger.EnterActi ons> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="FontSize" To="36" Duration=,,0:0:l" /> </Storyboard> </BeginStoryboard> </Trigger.EnterActi ons> <Trigger.ExitActions> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="FontSize" To="12" Durations:0:0.25" /> </Storyboard> </BeginStoryboard> </Trigger.ExitActions> </Trigger> </Style.Triggers> </Style> </StackPanel.Resources>
Анимация 885 <Button>Button No. 1</Button> <Button>Button No. 2</Button> <Button>Button No. 3</Button> <Button>Button No. 4</Button> <Button>Button No. 5</Button> <Button>Button No. 6</Button> <Button>Button No. 7</Button> <Button>Button No. 8</Button> <Button>Button No. 9</Button> </StackPanel> В этом случае элемент BeginStoryboard должен быть дочерним по отношению к коллекциям EnterActions и ExitActions, определяемым TriggerBase. Изменять размеры кнопки посредством изменения FontSize удобно, однако это нельзя считать общим решением для изменения размера элементов. Вероятно, в более общем решении следует задать преобразование для элемента, а затем ани- мировать это преобразование. Такой способ особенно удобен при реализации анимированных поворотов. При анимации преобразований в TargetName указывается либо элемент, со- держащий преобразование, либо само преобразование. Обычно анимируется пре- образование RenderTransform, а не LayoutTransform. Атрибут Targetproperty анимации ссылается на свойство RenderTransform анимируемого элемента. По умолчанию свойство RenderTransform содержит объект MatrixTransform, поэтому если вы не собираетесь анимировать отдельные свойства структуры Matrix, следует сначала задать RenderTransform другой тип преобразования (например, RotateTransform), а затем анимировать свойство этого преобразования. Следующий XAML-файл определяет стиль для элементов Button, в котором свойству RenderTransform объекта Button задается объект RotateTransform в кон- фигурации по умолчанию (то есть без поворота). Кроме того, свойству Render- TransformOrigin задается значение (0,5, 0,5), чтобы поворот выполнялся относи- тельно центра кнопки. ShakingButton.xaml <।_ _ ================================================== ShakingButton.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <StackPanel.Resources> <Style TargetType="{x:Type Button}"> <Setter Property=,,HorizontalAlignment" Value="Center" /> <Setter Property="Margin" Value=,,12" /> <Setter Property=,,RenderTransformOrigin" Value="0.5 0.5" /> <Setter Property="RenderTransform"> <Setter.Value> <RotateTransform /> </Setter.Value>
886 Глава 30. Анимация </Setter> <Sty1е.Triggers> <EventTrigger RoutedEvent=,,Button.Click"> <BeginStoryboard> <Storyboard TargetProperty="RenderTransform.Angle"> <DoubleAnimation From="-5" To="5" Duration=,,0:0:0.05n AutoReverse=,,True" RepeatBehavior="3x" FillBehavior=,,Stop" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Style.Triggers> </Style> </StackPanel,Resources> <Button>Button No. 1</Button> <Button>Button No. 2</Button> <Button>Button No. 3</Button> <Button>Button No. 4</Button> <Button>Button No. 5</Button> </StackPanel> Атрибут Targetproperty анимации равен RenderTransform.Angle, поэтому, разу- меется, анимация предполагает, что RenderTransform уже задан объект типа Rotate- Transform. За 0,05 секунды угол поворота изменяется от -5 до 5 градусов. Затем анимация инвертируется и повторяется еще два раза. Если замедлить анимацию, вы увидите, что кнопка сначала переходит из нормальной позиции к повороту в -5 градусов, а в конце перемещается обратно. На большой скорости этот дефект неразличим, но в других случаях кнопка обычно сначала анимируется от 0 до -5 градусов, затем повторно анимирует- ся между -5 и 5 градусами, после чего следует третья анимация с переходом от -5 до 0 градусов. Для управления анимацией в процессе воспроизведения используются клас- сы, производные от ControllableStoryboardAction. Следующая диаграмма классов показывает, какое место эти классы занимают в иерархии TriggerAction. Object Dispatcherobject (абстрактный) DependencyObject TriggerAction (абстрактный) SoundPlayerAction BeginStoryboard Control 1ableStoryboardAction (абстрактный) PauseStoryboard RemoveStoryboard ResumeStoryboard SeekStoryboard
Анимация 887 SetStoryboa rdSpeedRati о SkipStoryboardToFill StopStoryboard Чтобы использовать эти классы, следует оформить анимацию обычным обра- зом с BeginStoryboard и задать BeginStoryboard атрибут Name. Класс ControllableStory- boardAction определяет свойство BeginStoryboardName, ссылающееся на это имя. Как и BeginStoryboard, потомки ControllableStoryboardAction определяются как часть коллекции TriggerAction. В следующем автономном XAML-файле создается объект Rectangle, для кото- рого определяются два объекта RotateTransform — для поворота вокруг левого нижнего и правого нижнего углов. Кадровый план (определяемый обычным об- разом, не считая того, что в элементе BeginStoryboard определяется атрибут Name) использует DoubleAnimation для изменения угла первого преобразования Rotate- Transform т -90° до 0°, после чего второе преобразование DoubleAnimation изменяет угол второго преобразования RotateTransform от 0° до 90°. Создается впечатление, что прямоугольник сначала поднимается, а затем падает в другом направлении. ControllingTheStoryboard.xaml * <।_ _ =========================================================== Control 1ingTheStoryboard.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml "> <StackPanel> < !-- Панель Canvas для вывода анимированного прямоугольника --> <Canvas Width="350" Height="200"> <Rectangle Canvas.Left="150" Canvas.Top="50" Stroke="Black" StrokeThickness="4" Fill="Aqua" Width="50" Height="150"> <Rectangle.RenderT ransform> <TransformGroup> <RotateTransform x:Name="xforml" Angle="-90" CenterX="0" CenterY=,,150" /> <RotateTransform x:Name=,,xform2" CenterX="50" CenterY="150" /> </TransformGroup> </Rectangle.RenderT ransform> </Rectangle> </Canvas> < !-- Панель StackPanel с кнопками управления анимацией --> <StackPanel 0rientation='Horizontal" HorlzontalAlignment="Center"> <Button Name="btnBegin" Content="Begin" Margin="12" /> <Button Name="btnPause" Content="Pause" Margin="12" /> <Button Name="btnResume" Content="Resume" Margin="12" /> <Button Name="btnStop" Content="Stop" Margin="12" />
888 Глава 30. Анимация <Button Ndme="btnSkip" Content="Skip to End" Margin="12" /> <Button Name="btnCenter" Content="Skip to Center" Margin="12" /> </StackPanel> < !-- Секция Triggers для щелчков на кнопках --> <StackPanel.Triggers> <EventTrigger SourceName="btnBegin" RoutedEvent="Button.Click"> <BeginStoryboard Name="storybrd"> <Storyboard > <DoubleAnimation Storyboard.TargetName="xforml" Storyboard.TargetProperty="Angle" From="-90" To="0" Duration="0:0:5" /> <DoubleAnimation Storyboard.TargetName="xform2" Storyboa rd.Ta rgetProperty="Ang1e" BeginTime="0:0:5" From="0" To="90" Duration="0:0:5" /> </Storyboard> </BeginStoryboard> </EventTrigger> <EventTrigger SourceName="btnPause" RoutedEvent="Button.Click"> <PauseStoryboard BeginStoryboardName="storybrd" /> </EventTrigger> <EventTrigger SourceName="btnResume" RoutedEvent="Button.Click"> <ResumeStoryboard BeginStoryboardName="storybrd" /> </EventTrigger> <EventTrigger SourceName="btnStop" RoutedEvent="Button.Click"> <StopStoryboard BeginStoryboardName="storybrd" /> </EventTrigger> <EventTrigger SourceName="btnSkip" RoutedEvent="Button.Click"> <SkipStoryboardToFill BeginStoryboardName="storybrd" /> </EventTrigger> <EventTrigger SourceName="btnCenter" RoutedEvent="Button.Click"> <SeekStoryboard BeginStoryboardName="storybrd" 0ffset="0:0:5" /> </EventTrigger> </StackPanel,Triggers> </StackPanel> </Page> Под прямоугольником находятся шесть кнопок, каждой из которых присвоен атрибут Name. Секция StackPanel.Triggers содержит элементы EventTrigger, ассоци- ируемые с событиями Click каждой из этих кнопок. Кнопка Begin запускает ани- мацию. Кнопка Pause инициирует действие PauseStoryboard, приостанавливающее анимацию, а кнопка Resume возобновляет ее. Кнопка Stop возвращает прямоуголь- ник в исходное состояние. Кнопка Skip to End инициирует действие SkipStoryboard- ToFill с тем, что должно происходить с текущим значением FillBehavior. Действие SeekStory boa rd осуществляет переход к произвольной точке анимации; я выбрал позиционирование на середину.
Анимация 889 В исходной версии программы я определил секцию Triggers для каждой из кнопок и разместил в секциях Triggers отдельные элементы EventTriggers, но про- грамма не работала. Очевидно, все объекты EventTrigger, инициирующие и управ- ляющие кадровым планом, должны быть объединены в одной секции Triggers. Во многих демонстрационных XAML-программах анимация начинается сразу же после загрузки программы и продолжается до ее завершения. В таких фай- лах анимация инициируется событием Loaded. В качестве триггера может ис- пользоваться как событие Loaded анимируемого элемента, так и событие Loaded другого элемента — например, Panel или Раде. Следующий автономный XAML-файл является анимированной версией про- граммы RenderTransformAndLayoutTransform.xaml из предыдущей главы. Он пока- зывает, почему обычно анимируется RenderTransform с минимальным влиянием на окружение элемента, если только анимированный элемент не должен вли- ять на структуру макета — в этом случае определенно следует анимировать LayoutTransform. RenderTransformVersusLayoutTransform.xaml <।_ _ ======================================================================= RenderTransformVersusLayoutTransform.xaml (с) 2006 by Charles Petzold <StackPanel xmlns="http://schemas.microsoft.com/wi nfx/2006/xaml/presentati on" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" TextBlock.FontSize="18pt" > <!-- Секция RenderTransform --> <TextBlock TextAlignment=,,Center" Margin="24"> Animate <1taiic>RenderTransform</Italic> </TextBlock> <UniformGrid Rows="3" Columns="3"> <Button Content="Button" /> <Button Content="Button" /> <Button Content=,,Button" /> <Button Content="Button" /> <Button Content="Button"> <Button.RenderT ransform> <RotateTransform x:Name="xformr /> </Button.RenderTransform> </Button> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> </UniformGrid> <!-- Секция LayoutTransform --> <TextBlock TextAlignment="Center" Margin="24">
890 Глава 30. Анимация Animate <Ital1c>LayoutTransform</1tai 1c> </TextBlock> <UniformGrid Rows="3" Columns=,,3"> <Button Content^'Button" /> <Button Content^'Button" /> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" > <Button.LayoutTransform> <RotateTransform x:Name="xform2" /> </Button.LayoutT ransform> </Button> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> <Button Content="Button" /> </UniformGrid> <!-- Анимации --> <StackPanel.Triggers> <EventTrigger RoutedEvent="StackPanel,Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetName="xform2" Storyboard.TargetProperty="Angle" Duration="0:0:10" From="0" To="360" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard.TargetName="xforml" Storyboard.TargetProperty="Angle" Duration="0:0:10" From="0" To="360" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </StackPanel.Triggers> </StackPanel> Как вы, возможно, помните, в не-анимированной версии программы две па- нели UniformGrid 3x3 заполняются кнопками, а средняя кнопка каждой панели поворачивается — для первой используется RenderTransform, для второй Layout- Transform. В этой версии два угла поворота анимируются. Корневым элементом в этом файле является StackPanel, а две анимации вклю- чены в секцию Triggers элемента StackPanel с инициирующим событием Loaded. Целевыми свойствами анимаций являются сами преобразования, которым зада- ются атрибуты x:Name. Атрибут TargetProperty равен Angle, а анимации продолжа- ются неограниченно долго.
Анимация 891 Если вы используете программу вроде XAML Cruncher для экспериментов с анимацией, инициируемой событием Loaded, учтите, что при обработке файла методом XamIReader.Load и повторном создании элементов анимация начнется заново. В XAML Cruncher это происходит при каждом нажатии клавиши, изме- няющем текст файла. Возможно, стоит отключить автоматическую обработку и запускать разбор файла вручную клавишей F6. В следующем автономном XAML-файле определяется один объект Path на базе двух объектов EllipseGeometry, второй из которых поворачивается посредст- вом анимации. Соприкосновение кругов наглядно демонстрирует, как работает режим заполнения замкнутых областей EvenOdd. TotalEclipseOfTheSun.xaml Total EclipseOfTheSun.xaml (c) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Path Fill="Gray" Stroke="Black" StrokeThickness="3"> <Path.Data> <GeometryGroup> <EllipseGeometry Center="96 288" RadiusX="48" RadiusY="48" /> <EllipseGeometry Center="288 96" RadiusX="48" RadiusY="48"> <E11i pseGeometry.T ransform> <RotateTransform x:Name="rotate" CenterX="288" CenterY="288" /> </EllipseGeometry.Transform> </EllipseGeometry> </GeometryGroup> </Path.Data> </Path> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboa rd.TargetName="rotate" Storyboard.TargetProperty="Angle" From="0" To="360" Duration="0:0:5" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas> Следующая XAML-программа содержит объект TextBlock, для которого в сек- ции TransformGroup определяются преобразования ScaleTransform и RotateTransform.
892 Глава 30. Анимация Свойство ScaleX объекта ScaleTransform анимируется в диапазоне от 1 до -1; тем самым обеспечивается отражение текста относительно вертикальной оси (ими- тация поворота текста в трехмерном пространстве). Преобразование RotateTrans- form поворачивает объект вокруг центра. Две анимации обладают разной про- должительностью и воспроизводятся без ограничения по времени. AnimatedTextTransform.xaml <!- - ======================================================== AnimatedTextTransform.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml "> <TextBlock Text="XAML" FontSize="144pt" FontFamily="Arial Black" HorizontalAlignment="Center" Vertical Alignment="Center" RenderTransform0rigin="0.5 0.5"> <TextBlock.RenderTransform> <TransformGroup> <ScaleTransform x:Name="xformScale" /> <RotateTransform x:Name="xformRotate" /> </TransformGroup> </TextBlock.RenderT ransform> <TextBlock.T ri ggers> <EventTrigger RoutedEvent="TextBlock.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetName="xformScale" Storyboard.TargetProperty="ScaleX" From="l" To="-1" Duration="0:0:3" AutoReverse="True" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard.TargetName="xformRotate" Storyboard.TargetProperty="Angle" From="0" To="360" Duration="0:0:5" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </TextBlock.T ri ggers> </TextBlock> </Page> Во приводившихся ранее программах использовались анимации DoubleAnima- tion и ColorAnimation. На практике также часто используется PointAnimation — ани- мация между двумя объектами Point. Следующая автономная XAML-программа определяет объект Path из четырех кривых Безье.
Анимация 893 SquaringTheCircle.xaml SquaringTheCIrele.xaml (c) 2006 by Charles Petzold Canvas xmlns="http://schemas.mlcrosoft.com/wlnfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" RenderTransform="2 0 0 -2 300 300"> <Path StrokeThickness="3" Stroke="Blue" Fill="AliceBlue"> <Path.Data> <PathGeometry> <PathFigure x:Name="bezl" IsClosed="True"> <BezierSegment x:Name="bez2" /> <BezierSegment x:Name="bez3" /> <BezierSegment x:Name="bez4" /> <BezierSegment x:Name="bez5" /> </PathFigure> <PathGeometry.T ransform> <RotateTransform Angle="45" /> </PathGeometry.Transform> </PathGeometry> </Path.Data> </Path> Canvas.Triggers> CventTrigger RoutedEvent="Canvas .Loaded"> <BeginStoryboard> Ctoryboard RepeatBehavior="Forever" AutoReverse="True" > <PointAnimation Storyboard.TargetName="bezl" Story boa rd.Ta rgetProperty="Sta rtPoi nt" From="0 100" To="0 125" /> <PointAnimation Storyboard.TargetName="bez2" Storyboa rd.Ta rgetProperty="Po inti" From="55 100" To="62.5 62.5" /> <PointAnimation Storyboard.TargetName="bez2" Storyboa rd.Ta rgetProperty="Poi nt2" From="100 55" To="62.5 62.5" /> <PointAnimation Storyboard.TargetName="bez2" Storyboa rd.Ta rgetProperty="Poi nt3" From="100 0" To="125 0" /> <Poi ntAn i mat i on Storyboa rd.Ta rgetName="bez3" Storyboa rd.Ta rgetProperty="Poi nt1" From="100 -55" To="62.5 -62.5" /> <PointAnimation Storyboard.TargetName="bez3" Storyboard.TargetProperty="Point2" From="55 -100" To="62.5 -62.5" /> <Poi ntAni mati on Storyboa rd.Ta rgetName="bez3" Storyboa rd.Ta rgetProperty="Poi nt3" From="0 -100" To="0 -125" />
894 Глава 30. Анимация <Ро1 ntAn jmation Storyboa rd.Ta rgetName="bez4" Storyboa rd.Ta rgetProperty="Poi nt1" From="-55 -100" To="-62.5 -62.5" /> <PointAni mati on Storyboa rd.Ta rgetName="bez4" Storyboa rd.Ta rgetProperty="Poi nt2" From="-100 -55" To="-62.5 -62.5" /> <Po1 ntAn i ma11 on Storyboa rd.Ta rgetName="bez4" Storyboa rd.Ta rgetProperty="Poi nt3" From="-100 0" To="-125 0" /> <PointAnimation Storyboard.TargetName="bez5" Storyboa rd.Ta rgetProperty="Poi nt1" From="-100 55" To="-62.5 62.5" /> <PointAnimation Storyboard.TargetName="bez5" Storyboa rd.Ta rgetProperty="Poi nt2" From="-55 100" To="-62.5 62.5" /> <PointAnimation Storyboard.TargetName="bez5" Storyboa rd.Ta rget Property="Poi nt3" From="0 100" To="0 125" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas> Тринадцать объектов PointAnimation изменяют 13 точек, составляющих фигуру. (Последняя точка совпадает с первой.) Значения From определяют круг с цен- тром в точке (0, 0) и радиусом 100. Значения То определяют квадрат со сторо- ной 177 единиц, занимающий примерно ту же область, что и круг. Анимация превращает круг в квадрат, и наоборот. В соответствии со значениями То углы квадрата располагаются на сторонах, так что фигура больше напоминает ромб. Я решил, что стороны должны быть горизонтальными и вертикальными, но вместо изменения всех координат я про- сто определил для Path преобразование RotateTransform, поворачивающее фигуру на 45°. RenderTransform для панели Canvas перемещает начало координат в точку (300, 300) и удваивает размер фигуры. Класс DrawingContext используется для рисования при переопределении метода OnRender, определяемого UIEIement, а также при создании объектов типа Drawing- Visual. Многие методы рисования, определяемые DrawingContext, имеют перегру- женные версии с аргументами Animationclock. Таким образом можно создавать элементы, в которые изначально заложена поддержка анимации. Далее приводится пример класса, производного от FrameworkElement и пере- определяющего метод OnRender. AnimatedCircle.cs //------------------------------------------------ // AnimatedCircle.cs (с) 2006 by Charles Petzold //------------------------------------------------ using System;
Анимация 895 using System.Windows: using System.Windows.Media: using System.Windows.Media.Animation; namespace Petzold.RenderTheAnimation { class AnimatedCircle : FrameworkElement { protected override void OnRender(DrawingContext de) { DoubleAnimation anima = new DoubleAnimation(): anima.From = 0: anima.To = 100: anima.Duration = new Duration(TimeSpan.FromSecondsd)): anima.AutoReverse = true: anima.RepeatBehavior = RepeatBehavior.Forever: Animationclock clock = anima.CreateClockO: dc.DrawEllipse(Brushes.Blue, new Pen(Brushes.Red, 3), new Point(125, 125), null, 0, clock, 0, clock): } } } Экземпляр такого класса создается в XAML-файле или программном коде. Я включил его в следующий проект RenderTheAnimation, в котором также при- сутствует файл C# RenderTheAnimation.cs. RenderTheAnimation.cs //---------------------------------------------------- // RenderTheAnimation.cs (с) 2006 by Charles Petzold //---------------------------------------------------- using System: using System.Windows: namespace Petzold.RenderTheAnimation { class RenderTheAnimation : Window [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new RenderTheAnimation()): public RenderTheAnimation() { Title = "Render the Animation"; Content = new AnimatedCircle!): } } }
896 Глава 30. Анимация Следующий автономный XAML-файл обеспечивает анимацию объекта Ellipse, инициируемую событием Loaded самого объекта. Атрибут Targetproperty анима- ции равен Width, но свойство Height привязано к свойству Width, поэтому оба свой- ства изменяются одинаково. Pulse.xaml <।_ _ ========================================== Pulsing.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <E1lipse HorizontalAllgnment="Center" VerticalAlignment="Center" Width="48" Fill="Red" Height="{Binding RelativeSource={RelativeSource self}, Path=Width}"> <E1lipse.Triggers> <EventTrigger RoutedEvent="Ellipse.Loaded"> <BeginStoryboard> <Storyboa rd TargetProperty="Wi dth" RepeatBehavior="Forever"> <DoubleAnimation From="48" To="288" Duration="0:0:0.25" BeginTime="0:0:r' RepeatBehavior="2x" FillBehavior="Stop" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Е11ipse.Triggers> </Е11i pse> </Page> Атрибут BeginTime элемента DoubleAnimation равен одной секунде, поэтому в этом промежутке времени анимация не выполняется. Затем свойство Width (а через привязку — и свойство Height) быстро увеличивается от 0,5 до 3 дюй- мов и возвращается к величине 0,5 дюйма. Из-за атрибута RepeatBehavior это происходит дважды. У элемента Storyboard атрибут RepeatBehavior задан равным Forever, чтобы ани- мация DoubleAnimation повторялась неограниченно долго. Для каждой итерации BeginTime задерживает два «импульса» на одну секунду. В следующей программе представлен хитроумный визуальный эффект с объ- ектом Rectangle, анимируемым несколькими способами. Два объекта DoubleAnima- tion увеличивают ширину и высоту прямоугольника. В то же время два других объекта DoubleAnimation уменьшают присоединенные свойства Canvas.Left и Canvas. Тор, чтобы прямоугольник оставался на месте. Два объекта PointAnimation изме- няют свойства StartPoint и EndPoint объекта LinearGradientBrush. Изначально эти точки соответствуют левому верхнему и правому нижнему углам прямоуголь-
Анимация 897 ника, но перемещаются в правый верхний и левый нижний углы. Наконец, два объекта ColorAnimation меняют местами цвета, используемые при построении гра- диентной кисти. PulsatingRectangle.xaml Pul sat1ngRectanglе.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Rectangle Name="rect" Canvas.Left="96" Canvas.Top="96" Width="192" Height="192" Stroke="Black"> <Rectangle.Fi11> LinearGradientBrush x:Name="brush"> <Li nearGradi entBrush.Gradi entStops> <GradientStop Offset="0" Color="Red" /> <GradientStop Offset=,,l" Color="Blue" /> </LinearGradientBrush.Gradi entStops> </L1nearGradientBrush> </Rectangle.Fi11> </Rectangle> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetName="rect" Storyboard.TargetProperty="Width" From="192" To="204" Duration="0:0:0.1" AutoReverse="True" RepeatBehavior="Forever" /> <DoubleAni mat i on Storyboa rd.Ta rgetName="rect" Storyboa rd.Ta rget Property="Hei ght" From="192" To="204" Duration="0:0:0.1" AutoReverse="True" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard.TargetName="rect" Storyboard.TargetProperty="(Canvas.Left)" From="96" To="90" Duration="0:0:0.1" AutoReverse="True" RepeatBehavior="Forever" /> <DoubleAni mati on Storyboard.TargetName="rect" Storyboard.TargetProperty="(Canvas.Top)" From="96" To="90" Duration="0:0:0.1" AutoReverse="True" RepeatBehavior="Forever" />
898 Глава 30. Анимация <PointAnimation Storyboard.TargetName="brush" Storyboa rd.TargetProperty="StartPoint" From="0 0" To="l 0" Duration=,,0:0:5" AutoReverse="True" RepeatBehavior="Forever" /> <Po1ntAnimation Storyboard.TargetName="brush" Storyboard.TargetProperty="EndPoint" From="l 1" To="0 1" Durat1on="0:0:5" AutoReverse="True" RepeatBehavior="Forever" /> <ColorAnimation Storyboa rd.Ta rgetName="brush" Storyboa rd.Ta rgetProperty= "GradientStopsEO].Color" From="Red" To="Blue" Duration="0:0:1" AutoReverse="True" RepeatBehavior="Forever" /> <ColorAnimation Storyboa rd.TargetName="brush" Storyboa rd.Ta rgetProperty= "Gradi entStopsE1].Col or" From="Blue" To="Red" Durat1on="0:0:1" AutoReverse="True" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas> Восемь анимационных объектов обладают тремя разными значениями про- должительности, а их атрибуты RepeatBehavior равны Forever. Казалось бы, проще задать этот атрибут для Storyboard и удалить отдельные атрибуты RepeatBehavior, но это не приведет к желаемому результату — повторяться будет анимация, про- должающаяся 10 секунд (итоговая продолжительность самого длинного объекта анимации с учетом AutoReverse). Анимации с продолжительностью менее 10 се- кунд будут происходить только в начале каждого 10-секундного периода. Если вы хотите, чтобы несколько анимаций выполнялись бесконечно долго, переме- щение атрибута RepeatBehavior в Storyboard возможно только в том случае, если все отдельные анимации обладают одинаковой итоговой продолжительностью. Атрибут AutoReverse весьма удобен в бесконечно повторяющихся анимациях, потому что он помогает избежать нестыковок при повторениях. Впрочем, это не единственный способ обеспечения плавной анимации — чтобы добиться желае- мого эффекта, достаточно сделать так, чтобы конечная позиция (или размер и т. д.) одного анимированного объекта совпадала с начальной позицией (или другим свойством) другого анимируемого объекта. В следующей XAML-программе пять объектов EllipseGeometry используют- ся для определения пяти концентрических кругов (я использовал EllipseGeometry
Анимация 899 вместо Ellipse, так как для них проще сохранить центровку относительно одной точки). Десять объектов DoubleAnimation увеличивают свойства RadiusX и RadiusY пяти кругов. У всех 10 объектов DoubleAnimation свойство IsAdditive равно true, и во всех случаях размер увеличивается от 0 до 25. Так как величины радиусов составляют 0, 25, 50, 75 и 100 единиц, размер каждого круга в конечной позиции анимации совпадает с начальным размером следующего круга. Для завершения эффекта необходимо предусмотреть специальную обработку для внутреннего (наименьшего) и внешнего (наибольшего) круга. У внутрен- него круга в исходном состоянии RadiusX и RadiusY равны 0, а первый объект DoubleAnimation анимирует свойство StrokeThickness от 0 до значения, заданного в элементе Path (12,5), так что круг возникает «из ничего». Последний объект DoubleAnimation в длинном списке уменьшает свойство Opacity внешнего круга, в результате чего круг «растворяется». Поскольку все анимации обладают оди- наковым значением Duration, атрибут RepeatBehavior в данной программе можно переместить в родительский элемент Storyboard. ExpandingCircles.xaml <।_ _ =================================================== ExpandingCircles.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" Wi ndowTi 11e="Expand!ng Ci rcles"> <Canvas Width="400" Height="400" HorizontalAlignment="Center" VerticalAlignment="Center" > <!-- Внутренний круг --> <Path Name="pathlnner" Stroke="Red" StrokeThickness="12.5"> <Path.Data> <E11ipseGeometry x:Name="elipsi" Center="200 200" RadiusX="O" RadiusY="0" /> </Path.Data> </Path> <!-- Все круги, кроме внутреннего и внешнего --> <Path Stroke="Red" StrokeThickness="12.5"> <Path.Data> <GeometryGroup> <E11ipseGeometry x:Name="elips2" Center="200 200" RadiusX="25" RadiusY="25" /> <E11ipseGeometry x:Name="elips3" Center="200 200" RadiusX="50" RadiusY="50" /> <E11ipseGeometry x:Name="elips4" Center="200 200" RadiusX="75" RadiusY="75" /> </GeometryGroup> </Path.Data> </Path>
900 Глава 30. Анимация <!-- Внешний круг --> <Path Name="pathOuter" Stroke="Red" StrokeThickness="12.5"> <Path.Data> <E11ipseGeometry x:Name="elips5" Center="200 200" RadiusX="100" RadiusY="100" /> </Path.Data> </Path> <Canvas.Triggers> <EventTrigger RoutedEvent="Canvas.Loaded"> <BeginStoryboard> <Storyboard RepeatBehavior="Forever"> <DoubleAnimation Storyboard.TargetName="pathInner" Storyboard.TargetProperty="StrokeThickness" From="0" Duration="0:0:5" /> <DoubleAni mati on Storyboa rd.Ta rgetName="eli ps1" Storyboard.TargetProperty="RadiusX" From="0" To="25" IsAdditive="True" Duration^'0:0:5" /> <DoubleAni mat i on Storyboa rd.Ta rgetName="eli ps1" Storyboard.TargetProperty="Radi usY" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimation Storyboard.TargetName="elips2" Storyboa rd.Ta rgetProperty="RadiusX" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimation Storyboard.TargetName="elips2" Storyboa rd.Ta rgetProperty="RadiusY" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimation Storyboard.TargetName="elips3" Storyboa rd.Ta rgetProperty="RadiusX" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimation Storyboard.TargetName="elips3" Storyboa rd.Ta rgetProperty="RadiusY" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimation Storyboard.TargetName="elips4" Storyboard.TargetProperty="RadiusX" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAni mati on Storyboa rd.Ta rgetName="eli ps4" Storyboa rd.Ta rgetProperty="RadiusY" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <DoubleAnimati on Storyboa rd.Ta rgetName="eli ps5"
Анимация 901 Storyboard.TargetProperty="RadiusX" From="0" To="25" IsAdditive=,,True" Duration="0:0:5" /> <DoubleAnimation Storyboard.TargetName="el ips5" Storyboard.TargetProperty="RadiusY" From="0" To="25" IsAdditive="True" Duration="0:0:5" /> <Doubl eAni mat ion Storyboard JargetName-'pathOuter'' Storyboard.TargetProperty="Opaci ty" From="r' To="0" Duration="0:0:5" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Canvas.Triggers> </Canvas> </Page> Конечно, от части объектов DoubleAnimation можно было бы избавиться по- средством привязок между свойствами RadiusX и RadiusY отдельных элементов EllipseGeometry. В отдельных случаях группу одновременных анимаций удается заменить одной анимацией, но такая замена потребует некоторых усилий, а воз- можно, и применения математики. Прототипом следующей программы стал файл Infinity.xaml (см. главу 28). В этой главе кривые Безье использовались для рисо- вания символа бесконечности, который закрашивался кистью LinearGradientBrush, образованной семью цветами радуги. В этой главе мы анимируем кисть, сдвигая вправо цвета градиента. В следующем файле определены семь объектов DoubleAnimation, анимирующие свойство Offset семи элементов Gradientstop кисти. Все объекты DoubleAnimation обладают одинаковой продолжительностью (7 секунд, но с утроенной скоростью из-за значения SpeedRatio), одинаковыми значениями From и То (0 и 1), но раз- ными значениями BeginTime от 0 до 6 секунд. InfinityAnimationl.xaml InfinityAnimationl.xaml (с) 2006 by Charles Petzold <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Path Canvas.Left="150" Canvas.Top="150" StrokeThickness="25"> <Path.Data> <PathGeometry> <PathGeometry.F i gures> <PathFigure StartPoint="0 -100"> <PolyBezierSegment Points=" -55 -100. -100 -55. -100 0. -100 55. -55 100. 0 100. 55 100. 100 50. 150 0. 200 -50. 245 -100. 300 -100.
902 Глава 30. Анимация 355 -100. 400 -55. 400 0. 400 55. 355 100, 300 100, 245 100. 200 50. 150 0. 100 -50. 55 -100, 0 -100" /> </PathFigure> </PathGeometry.F i gures> </PathGeometry> </Path.Data> <Path.Stroke> <LinearGradlentBrush x:Name="brush"> <Li nearGradlentBrush.Gradi entStops> <GradientStop Color="Red" /> <GradientStop Color="Orange" /> <GradientStop Color="Yellow" /> <GradientStop Color="Green" /> <GradientStop Color="Blue" /> <GradientStop Color="Indigo" /> <GradientStop Color="Violet" /> </Linea rGradi entBrush.Gradi entStops> </LinearGradientBrush> </Path.Stroke> <Path.Triggers> <EventTrigger RoutedEvent="Path.Loaded"> <BeginStoryboard> <Storyboard TargetName="brush" SpeedRatio="3"> <DoubleAnimation Storyboa rd.Ta rgetProperty= "GradientStops[0].Offset" From="0" To="l" Duration="0:0:7" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard.TargetProperty= "GradientStops[l].Offset" From="0" To="l" Duration="0:0:7" BeginTime="0:0:l" RepeatBehavior="Forever" /> <DoubleAnimation Storyboa rd.Ta rgetProperty= "GradientStops[2].Offset" From="0" To="l" Duration="0:0:7" BeginTime="0:0:2" RepeatBehavior="Forever" /> <DoubleAnimation Storyboa rd.Ta rget Property= "GradientStops[3].Offset" From="0" To="l" Duration="0:0:7" BeginTime="0:0:3" RepeatBehavior="Forever" /> <DoubleAnimation Storyboa rd.Ta rget Property= "GradientStops[4].Offset" From="0" To="l" Duration="0:0:7"
Анимация 903 BeginTime="0:0:4" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard.TargetProperty= "GradientStops[5].Offset" From="0" To"!" Duration=,,0:0:7" BeginTime="0:0:5" RepeatBehavior="Forever" /> <DoubleAnimation Storyboard.TargetProperty= "GradientStops[6].Offset" From="0" To="l" Duration="0:0:7" BeginTime="0:0:6" RepeatBehavior="Forever" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Path.Triggers> </Path> </Canvas> В начале программы у всех объектов Gradientstop свойство Offset по умол- чанию равно 0. Обычно это приводит к тому, что весь символ бесконечности закрашивается последним цветом Gradientstop в списке, то есть фиолетовым. Од- нако немедленно начинающаяся первая анимация присваивает первому объек- ту Gradientstop ненулевое значение Offset, в результате чего символ бесконеч- ности окрашивается в красный цвет. Через секунду (а вернее, через 1 /3 секунды из-за SpeedRatio) значение Offset первого объекта Gradientstop равно приблизи- тельно 0,14. Здесь начинается вторая анимация, окрашивающая начало объекта в оранжевый цвет. Так происходит последовательная активизация всех цветов. После запуска всех объектов DoubleAnimation цвета плавно переходят друг в друга.
31 Работа с графикой Компьютерная графика традиционно была разделена на две области: растровую и векторную. Растровые изображения строятся из отдельных точек (пикселов), а векторные — из отрезков, кривых и заполненных областей. Два класса, произ- водных от FrameworkElement и используемых для отображения графических объ- ектов, соответствуют этой классификации. Класс Image (впервые упоминавший- ся в главе 3) обычно применяется для вывода точечных рисунков, тогда как классы, производные от Shape (см. главы 3 и 27), предоставляют самые простые средства отображения векторной графики. Как правило, при повседневной ра- боте с графикой вполне хватает Image и классов, производных от Shape. Однако в действительности графические функции WPF не столь четко де- лятся между растровой и векторной графикой. Действительно, класс Image чаще всего используется для отображения точечных рисунков, но этим его возможности не ограничиваются. Класс Image также применяется для отображения объектов типа Drawingimage; вы получили некоторое представление о данной возможности в программе YellowPad из главы 22, где класс Drawingimage использовался для отображения подписи. Объект Drawingimage всегда базируется на основе Drawing, однако класс Drawing не ограничивается векторной графикой. Фактически объ- ект Drawing может быть комбинацией векторных и растровых элементов с видео. В этой главе рассматриваются взаимосвязи между растровой и векторной графикой в WPF; кроме того, в ней завершается тема кистей, начатая в главе 2. В той начальной главе было показано, как создаются однородные и градиентные кисти. Однако кисти также можно создавать на базе объектов Drawing, точечных рисунков и объектов типа Visual. Поскольку класс UIEIement является производ- ным от Visual, за основу для создания кисти можно взять элемент TextBlock или элемент управления Button. Начнем с растровых изображений. В WPF они представлены абстрактным классом BitmapSource и его потомками. Классы BitmapSource и Drawingimage (обыч- но, но не всегда используемый для отображения векторной графики) напрямую наследуют от ImageSource, как показано в следующей иерархии классов: Object Dispatcherobject (абстрактный) Dependencyobject
Работа с графикой 905 Freezable (абстрактный) Animatable (абстрактный) ImageSource (абстрактный) BitmapSource (абстрактный) BitmapFrame (абстрактный) Bitmapimage CachedBitmap ColorConvertedBi tmap CroppedBitmap FormatConvertedBi tmap RenderTargetBitmap TransformedBitmap Drawingimage Чтобы использовать элемент Image для отображения одного из этих объек- тов, его свойству Source задается объект типа ImageSource. Класс ImageSource оп- ределяет свойства Height и Width, доступные только для чтения, в аппаратно-не- зависимых единицах. BitmapSource — первый класс иерархии, однозначно специализирующийся на растровых изображениях. Он определяет свойства PixelWidth и PixelHeight (раз- меры рисунка), свойства DpiX и DpiY (разрешение) и свойство Format типа Pixel- Format (формат рисунка). Все перечисленные свойства доступны только для чте- ния. За исключением некоторых экзотических случаев, свойству Format задается одно из 26 статических свойств класса PixelFormats. Например, значение Pixel- Formats. Bgr32 означает 32 бита на пиксел с 1-байтовыми значениями синей, зеле- ной и красной составляющих и 1 неиспользуемым байтом на пиксел, а значение PixelFormats.Gray8 — 8 бит на пиксел в оттенках серого. Если растровое изобра- жение содержит цветовую таблицу, ее можно получить из свойства Palette объ- екта BitmapSource. Свойство Palette относится к типу BitmapPalette, содержащему свойство Colors типа IList<Color>. Из всех потомков BitmapSource чаще всего используется класс Bitmapimage — в основном потому, что он имеет конструктор, получающий объект Uri. Этот кон- структор (и свойство UriSource) позволяет загрузить растровое изображение из локального файла, файла в сети или файла, внедренного в приложение. Поддер- живаются форматы BMP, JPEG, GIF, TIFF и PNG. Программа ShowMyFace в главе 3 содержит следующий фрагмент кода для создания объекта Bitmapimage на базе объекта с моего веб-сайта и его последую- щего отображения в элементе Image: Uri uri = new Uri("http://www.charlespetzold.com/PetzoldTattoo.jpg"): Bitmapimage bitmap = new Bitmaplmage(uri): Image img = new ImageO; img.Source = bitmap; В коде XAML свойству Source элемента Image просто задается строка URI: <Image Source="http://www.charlespetzold.com/PetzoldTattoo.jpg" /> В программном коде также можно создать Bitmapimage конструктором без параметров, а затем загрузить изображение, задавая свойство UriSource или
906 Глава 31. Работа с графикой StreamSource. Используйте то свойство, которое вам покажется более удобным, но заключите код задания свойства между вызовами Beginlnit и Endlnit: Bitmapimage bitmap = new Bitmaplmage(); bitmap.Beglnlnlt(): bitmap.UriSource = uri; bitmap.EndlnitO: У такого решения есть дополнительные преимущества — изображение мож- но поворачивать с приращением 90° во время загрузки: Bitmapimage bitmap = new BitmapImageC): bitmap.BeginlnitO; bitmap.UriSource = uri: bitmap.Rotation = Rotation.Rotate90; bitmap.EndlnitO; В XAML поворот Bitmapimage осуществляется выделением свойства Source объекта Image в элемент свойства, в который заключается элемент Bitmapimage: <Image> <Image.Source> <Bi tmaplmage UriSource="http://www.charlespetzold.com/PetzoldTattoo.jpg" Rotation=,,Rotate90" /> </Image.Source> </Image> Задавая свойство SourceRect элемента Bitmapimage, можно выбрать прямо- угольный участок более крупного изображения: <Image> <Image.Source> <Bitmaplmage UriSource= "http://www.char1espetzold.com/PetzoldTattoo.jpg" SourceRect="150 100 200 200" /> </Image.Source> </Image> Кроме загрузки существующих растровых изображений в память, вы также можете создавать растровые объекты в программном коде. Существует два раз- ных способа размещения графики на созданных растровых объектах: выполне- нием графических операций или передачей фактических данных, составляющих изображение. Для сохранения изображений, созданных подобным образом, ис- пользуется метод Save одного из потомков BitmapEncoder. Чтобы создать растровое изображение, рисунок которого состоит из графи- ческих объектов, выводимых вашей программой, вы сначала создаете объект типа RenderTargetBitmap: RenderTargetBitmap renderbitmap = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Default);
Работа с графикой 907 Первые два аргумента определяют ширину и высоту изображения в пиксе- лах. Следующие два аргумента задают горизонтальное и вертикальное разреше- ние в точках на дюйм (им можно задать любые значения на ваше усмотрение). Последний аргумент задает формат пикселов. При работе с RenderTargetBitmap не- обходимо использовать либо формат PixelFormats.Default, либо PixelFormats.Pbgra32. В последнем варианте каждый пиксел представляется 4 байтами в порядке «си- ний-зеленый-красный-альфа», а значения трех цветовых составляющих заранее умножаются на коэффициент прозрачности, определяемый альфа-каналом. В момент создания RenderTargetBitmap изображение полностью прозрачно. Класс RenderTargetBitmap содержит метод Render с аргументом типа Visual, поэто- му графические операции с созданным объектом выполняются практически так же, как и графические операции с принтером. Метод Render может вызываться многократно. Чтобы удалить рисунок и восстановить растровый объект в исход- ном состоянии, вызовите метод Clear. Следующая программа использует класс RenderTargetBitmap для создания рас- трового изображения в форме квадрата со стороной 100 пикселов. Затем на со- зданном изображении рисуется прямоугольник с закругленными углами. Фон окна окрашивается в цвет хаки; это позволяет увидеть, что углы остаются про- зрачными. DrawGraphicsOnBitmap.cs //----------------------------------------------------- // DrawGraphicsOnBitniap.es (с) 2006 by Charles Petzold // using System: using System.Windows: using System.Windows.Controls: using System.Windows.Input: using System.Windows.Media: using System.Windows.Media.Imaging: namespace Petzold.DrawGraphi csOnBitmap { public class DrawGraphicsOnBitmap : Window { [STAThread] public static void MainO { Application app = new Application): app.Run(new DrawGraphicsOnBitmapO): } public DrawGraphicsOnBitmapO { Title = "Draw Graphics on Bitmap": // Назначение фона для демонстрации прозрачности Background = Brushes.Khaki:
908 Глава 31. Работа с графикой // Создание объекта RenderTargetBitmap RenderTargetBitmap renderbitmap = new RenderTargetBitmapUOO. 100, 96. 96, PixelFormats.Default); // Создание объекта DrawingVisual DrawingVisual drawvis = new DrawingVisual0; DrawingContext de = drawvis.RenderOpenO; de.DrawRoundedRectangle(Brushes.Blue, new Pen(Brushes.Red, 10), new Rect(25, 25. 50, 50), 10. 10); de.Closet); // Вывод DrawingVisual на RenderTargetBitmap renderbitmap.Render(drawvi s); // Создание объекта Image object и задание // его свойству Source построенного рисунка Image Img = new ImageO; img.Source = renderbitmap; // Назначение объекта Image содержимым окна Content = img; } } } По умолчанию элемент Image растягивает растровое изображение для запол- нения всей доступной области. Цвета новых пикселов определяются интерполя- цией, чтобы избежать появления «квадратиков». При этом изображения с резко очерченными краями могут казаться размытыми. Чтобы заставить Image отобра- жать растровое изображение в его естественных размерах, задайте свойство Stretch: img.Stretch = Stretch.None; Растровый объект, как и печатная страница, может использоваться для ото- бражения элементов. Следующая программа создает UniformGrid и размещает на его поверхности 32 объекта ToggleButton. Кнопки не имеют содержимого, но об- ратите внимание, что у некоторых кнопок установлено свойство IsChecked, вслед- ствие чего они окрашиваются более темным цветом. DrawButtonsOnBitmap.cs //---------------------------------------------------- // DrawButtonsOnBitmap.cs (с) 2006 by Charles Petzold //---------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media;
Работа с графикой 909 using System.Windows.Media.Imaging; namespace Petzold.DrawButtonsOnBi tmap { public class DrawButtonsOnBitmap : Window { [STAThread] public static void MainO { Application app = new Application); app.Run(new DrawButtonsOnBitmap()); } public DrawButtonsOnBitmap() { Title = "Draw Buttons on Bitmap"; // Создание элемента UniformGrid для размещения кнопок UniformGrid unigrid = new UniformGrid(); unigrid.Columns = 4; // Создание 32 объектов ToggleButton на UniformGrid for (int i = 0; i < 32; i++) { ToggleButton btn = new ToggleButtonO; btn.Width = 96; btn.Height = 24; btn.IsChecked = (i < 4 | i > 27) x (И4==0 | И4==3); unigrid.Children.Add(btn); } // Определение размеров UniformGrid unigrid.Measure(new Size(Double.PositiveInfinity. Double.Positiveinfinity)); Size szGrid = unigrid.DesiredSize; // Размещение UniformGrid unigrid.Arrange(new Rect(new Point(0, 0), szGrid)); // Создание объекта RenderTargetBitmap RenderTargetBitmap renderbitmap = new RenderTargetBitmap((int)Math.Cei1ing(szGrid.Wi dth), (int)Math.Ceiling(szGrid.Height), 96, 96, Pixel Formats.Default); // Вывод UniformGrid на RenderTargetBitmap renderbi tmap.Render(uni gri d); // Создание объекта Image и задание его свойства Source Image img = new ImageO; img.Source = renderbitmap; // Назначение объекта Image содержимым окна Content = img; } } }
910 Глава 31. Работа с графикой Другой подход к созданию растровых изображений на программном уровне основан на передаче массива битов изображения. Для этого используется стати- ческий метод Create класса BitmapSource: BitmapSource bitmap = BitmapSource.Create(width. height. 96. 96. pixformat, palette, array, stride); Первые два аргумента определяют ширину и высоту растрового объекта в пик- селах. Следующие два аргумента задают горизонтальное и вертикальное расши- рение в точках на дюйм. Пятый аргумент обычно представляет собой статиче- ское свойство класса PixelFormats. Для растровых форматов, требующих цветовой таблицы — а конкретно для растровых изображений, созданных с использованием статических свойств Pixel- Formats, начинающихся со слова Indexed, — следующий аргумент относится к ти- пу BitmapPalette и представляет собой коллекцию объектов Color. Фактическое количество элементов в коллекции может быть меньше подразумеваемого фор- матом. Например, PixelFormats.Indexed4 означает, что каждый пиксел кодируется 4 битами, поэтому коллекция BitmapPalette может содержать не более 16 цветов. Было бы нелогично хранить в ней всего четыре цвета, потому что тогда было разумнее использовать формат PixelFormats.Indexed? и кодировать пиксел 2 бита- ми. Прежде чем создавать собственный объект BitmapPalette, присмотритесь к го- товым объектам BitmapPalette, доступным в виде статических свойств класса BitmapPalette. Для форматов, не требующих цветовой таблицы, этот аргумент за- дается равным null. Независимо от выбранного формата, каждая строка пикселов изображения состоит из целого числа байтов. Например, если вы создаете изображение ши- риной в 5 пикселов формата PixelFormats.Indexed4, для хранения каждого пик- села требуется 4 бита. Умножаем 5 пикселов на 4 бита, получаем 20 бит, или 2,5 байта. Так как каждая строка должна занимать целое количество байтов, оно равно 3. Последние 4 бита третьего байта каждой строки игнорируются. В последнем аргументе BitmapSource.Create (с именем stride) обычно передает- ся целый размер строки пикселов в байтах (3 для рассмотренного примера). Впрочем, аргументу stride также можно задать целое значение, большее вычис- ленного (в ранних версиях Windows значения stride должны были быть кратны 4, чтобы повысить эффективность обработки растровых изображений на 32-разряд- ных процессорах; в WPF такое требование отсутствует). Седьмой аргумент BitmapSource.Create — array — представляет собой одномер- ный числовой массив. Для массивов типа byte количество элементов в массиве равно stride х высота растра в пикселах. Для этой цели может использоваться массив short, ushort, int или uint; в зависимости от типа число элементов может уменьшиться вдвое или увеличиться вчетверо. Младший байт многобайтовых числовых элементов представляет крайний левый пиксел. Если значение stride нечетно, то, например, элемент массива ushort будет содержать информацию для конца одной строки и начала другой строки пикселов; это разрешено. Запреще- но только хранение в одном байте данных, относящихся к двум разным строкам.
Работа с графикой 911 Следующая программа создает растровое изображение в формате PixelFormats. Indexed8. Цветовая таблица содержит 256 элементов, которые представляют со- бой разные сочетания красного и синего. Растровый квадрат из 256 пикселов содержит однобайтовые пикселы, индексирующие цветовую таблицу для отобра- жения различных цветовых комбинаций. Левый верхний угол окрашен в чер- ный цвет, а правый нижней — в малиновый. CreateIndexedBitmap.cs //----------------------------------------------------- // CreatelndexedBitmap.cs (с) 2006 by Charles Petzold //----------------------------------------------------- using System; using System.Collections.Generic: using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; usi ng System.Wi ndows.Medi a.Imagi ng; namespace Petzold.CreatelndexedBitmap { public class CreatelndexedBitmap : Window { [STAThread] public static void MainO { Application app = new Application(); app.Run(new CreatelndexedBitmap()); } public CreatelndexedBitmapO { Title = "Create Indexed Bitmap"; // Создание палитры из 256 сочетаний красного и синего List<Color> colors = new List<Color>(); for (int г = 0; r < 256; r += 17) for (int b = 0; b < 256; b += 17) colors.Add(Color.FromRgb((byte)r. 0, (byte)b)): BitmapPalette palette = new BitmapPalette(colors); // Создание массива битов изображения byte[] array = new byte[256 * 256]; for (int x = 0; x < 256; x++) for (int у = 0; у < 256; y++) array[256 * у + x] = (byte)(((int)Math.Round(y / 17.0) « 4) | (int)Math.Round(x / 17.0));
912 Глава 31. Работа с графикой // Создание растрового объекта BitmapSource bitmap = BitmapSource.Create(256, 256. 96, 96, PixelFormats.Indexed8, palette, array, 256); // Создание объекта Image и задание свойства Source Image img = new ImageO; img.Source = bitmap; // Назначение объекта Image содержимым окна Content = img; } } } Следующая программа рисует похожее изображение, но с гораздо большей точностью, потому что в ней используется формат PixelFormats.Bgr32. Каждый пиксел представляется 4 байтами, поэтому для хранения данных пикселов в про- грамме определяется массив int. Старший байт каждого 4-байтового значения в этом формате не используется. CreateFullColorBitmap.es //-------------------------------------------------------- // CreateFullColorBitmap.cs (с) 2006 by Charles Petzold //-------------------------------------------------------- using System: using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; namespace Petzold.CreateFul1 ColorBitmap { public class CreateFulIColorBitmap : Window { [STAThread] public static void MainO { Application app = new ApplicationO; app.Run(new CreateFul1 ColorBitmap()); } public CreateFul IColorBitmapQ { Title = "Create Full-Color Bitmap"; // Создание массива битов изображения int[] array = new int[256 * 256]; for (int x = 0; x < 256; x++) for (int у = 0; у < 256: y++)
Работа с графикой 913 { int b = х: int g = 0; int г = у: array[256 * у + х] = b | (д « 8) | (г « 16): } // Создание растрового объекта BitmapSource bitmap = BitmapSource.Create(256, 256, 96, 96, Pixel Formats.Bgr32, null, array, 256 * 4); // Создание объекта Image и задание свойства Source Image img = new Image(): img.Source = bitmap; // Назначение объекта Image содержимым окна Content = img; } } } Многие классы, производные от BitmapSource, создают новые растровые изобра- жения на базе существующих. Многие из них могут использоваться в XAML. Например, следующая автономная программа XAML использует класс Cropped- Bitmap для загрузки изображения с моего сайта и обрезки его по размерам, ука- занным в свойстве SourceRect. Изображение используется в качестве источника для класса FormatConvertedBitmap, преобразующего его формат оттенков серого с кодировкой 2 бита на пиксел. Результат используется в качестве источника для преобразования TransformedBitmap, поворачивающего изображение на 90°. Нако- нец, объект Image выводит результат — обрезанное, преобразованное и поверну- тое изображение. ConvertedBitmapChain.xaml ConvertedBitmapChain.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Image> <Image.Source> <T ransformedBi tmap> < T ransformedBi tmap.T ransform> <RotateTransform Angle="90" /> < /TransformedBi tmap.T ransform> < T ransformedBi tmap.Source> <FormatConvertedBitmap DestinationFormat="Gray2"> <FormatConvertedBi tmap.Source>
914 Глава 31. Работа с графикой <CroppedB1tmap Source= "http://www.charlespetzold.com/PetzoldTattoo.jpg" SourceRect="120 80 220 200" /> </FormatConvertedBitmap.Source> </FormatConvertedBitmap> </T ransformedBi tmap.Source> </TransformedBitmap> </Image.Source> </Image> </Page> Первая иерархия классов, представленная в этой главе, базировалась на Image- Source — типа объекта, задаваемого свойству Soruce элемента Image. Приведу ее снова (классы, производные от BitmapSource, заменены многоточием): Object Dispatcherobject (абстрактный) Dependencyobject Freezable (абстрактный) Animatable (абстрактный) ImageSource (абстрактный) BitmapSource (абстрактный) Drawingimage Из этой иерархии следует, что свойству Source элемента Image также может быть задан объект типа Drawingimage. Не следует полагать, что класс Drawingimage поддерживает только векторную графику — в общем случае Drawingimage может содержать векторную графику, растровую графику и видео. Drawingimage занимает особое положение в этой иерархии. Похоже, этому клас- су суждено выполнять функции промежуточного носителя для обмена вектор- ной графикой — своего рода XAML-аналога метафайлов. Пока нет явных указа- ний на то, что Microsoft готовит Drawingimage большое будущее, но представле- ние о Drawingimage как о разновидности графических метафайлов недалеко от истины. Концепция объекта Image, на котором отображается векторная графика, по- началу кажется довольно необычной, поэтому начнем с простого примера. ImageDisplaysVectorGraphics.xaml <।_ _ ============================================================== ImageDisplaysVectorGraphics.xaml (c) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Image Stretch="None"> <Image.Source> <DrawingImage> <Drawi ng Image.Drawlng> <GeometryDrawl ng Brush="Blue">
Работа с графикой 915 <GeometryDrawi ng.Pen> <Pen Brush="Red" Thickness="5" /> </GeometryDrawi ng.Pen> <GeometryDrawi ng.Geometry> <ElljpseGeometry Center="0r0" RadiusX=,,50" Rad1usY="50" /> </GeometryDrawi ng.Geometry> </GeometryDrawing> </Drawi nglmage.Drawlng> </DrawingImage> </Image.Source> </Image> </Page> Свойство Source элемента Image выделяется в элемент свойства Image.Source, и ему задается объект типа Drawingimage, обладающий всего одним изменяемым свойством Drawing типа Drawing. Одним из классов, производных от Drawing, яв- ляется GeometryDrawing. GeometryDrawing состоит из объекта Geometry (см. главу 28), а также кисти и пера, используемых для вывода этого объекта. В рассмотренном примере свой- ству Geometry объекта GeometryDrawing задается объект EllipseGeometry. Свойство Geometry элемента GeometryDrawing не обязательно выделять в элемент свойства — вместо этого можно воспользоваться синтаксисом разметки. Следую- щая разметка описывает треугольник прямо в начальном теге GeometryDrawing: <GeometryDrawing Brush="Blue" Geometry="M 0 0 L 100 0 L 0 100 Z"> При анализе файла ImageDisplaysVectorGraphics.xaml удобно начать с внутрен- него элемента и постепенно продвигаться наружу: файл начинается с объекта Geometry, а конкретно — EllipseGeometry. Геометрический объект в действитель- ности представляет собой набор координат точек. В сочетании с объектами Brush и Реп он превращается в GeometryDrawing. Последний выходит за рамки аналитиче- ской геометрии и обладает атрибутами вывода (цвет, размеры пера). Как вскоре будет показано, класс GeometryDrawing является производным от Drawing. Объект Drawingimage всегда создается на базе объекта Drawing. Класс Drawingimage содер- жит конструктор, получающий объект Drawing, и свойство Drawing типа Drawing. От ImageSource класс Drawingimage наследует свойства Width и Height, доступные только для чтения; они определяют ширину и высоту изображения в аппаратно- независимых единицах. В данном примере оба свойства равны 105 единицам (диаметр эллипса плюс 2,5 единицы толщины пера с каждой стороны). У элемента Image свойству Stretch задано значение None. Это означает, что эллипс будет выводиться в своих фактических размерах (квадрат со стороной 105 единиц). Если удалить атрибут Stretch, эллипс примет максимально возмож- ные размеры при сохранении исходных пропорций. Если задать свойство Stretch равным Fill, класс Image заполняет всю доступную область, не обращая внима- ния на пропорции.
916 Глава 31. Работа с графикой Как видно из следующей иерархии классов, GeometryDrawing — один из пяти классов, производных от Drawing: Object Dispatcherobject (абстрактный) Dependencyobject Freezable (абстрактный) Animatable (абстрактный) Drawing (абстрактный) DrawlngGroup GeometryDrawing GlyphRunDrawIng ImageDrawIng VideoDrawing Класс DrawingGroup играет очень важную роль. Он определяет свойство Children, в котором хранится коллекция объектов Drawing. Именно так происходит объ- единение векторной и растровой графики в одно составное изображение. Очень часто при упоминании объектов типа Drawing в документации WPF в действи- тельности речь идет о типе DrawingGroup. Другие четыре класса, производные от Drawing, имеют более специализиро- ванный характер. Как вы видели в предыдущем XAML-файле, объект Geometry- Drawing объединяет объекты Geometry, Brush и Реп. Класс GlyphRunDrawing объеди- няет объект GlyphRun с кистью. Класс GlyphRun определяет последовательность символов определенного размера и начертания, но этот класс очень сложен в ис- пользовании, поэтому в книге он не рассматривается. Объект ImageDrawing обычно представляет растровое изображение, заданное свойством ImageSource. Для задания определенных размеров изображения необ- ходимо использовать свойство Rect. Здесь он также не рассматривается. В нескольких последних абзацах обсуждался класс Drawingimage, а сейчас только что был представлен новый класс ImageDrawing. Сходство между ними не ограничивается только именами: объект ImageDrawing может задаваться свойст- ву Drawing объекта Drawingimage. Возможно, следующая таблица немного прояс- нит ситуацию (пли наоборот, запутает ее окончательно). Класс Производный от Содержит свойство Типа Drawingimage ImageSource Drawing Drawing ImageDrawing Drawing ImageSource ImageSource В следующем автономном XAML-файле классы Drawingimage и ImageDrawing используются именно так, как предполагали их разработчики (а не вторичны- ми способами, обусловленными циклической природой определений свойств). XAML-файл содержит элемент Image со свойством Source, которому задан объ- ект типа Drawingimage. Свойству Drawing объекта Drawingimage задается объект DrawingGroup, в число потомков которого входит объект ImageDrawing, связанный с растровым изображением, и три объекта GeometryDrawing, которые рисуют рам- ку вокруг изображения и подвеску для ее крепления на стене.
Работа с графикой 917 PictureAndFrame.xaml < I _ _ = === = = ============================= ========: PictureAndFrame.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml "> <Image Stretch="None"> <Image.Source> <DrawingImage> <Drawi nglmage.Drawl ng> <DrawingGroup> <!-- Изображение фиксированного размера --> <ImageDrawing Rect="5 5 200 240" ImageSource= "http://www.charlespetzold.com/PetzoldTattoo.jpg" /> <!-- Точечное перо для имитации узорного края --> <GeometryDrawing> <GeometryDrawi ng.Pen> <Pen Brush="DodgerBlue" Thickness="10" DashCap="Round"> <Pen.DashStyle> <DashStyle Dashes="0 1" /> </Pen.DashStyle> </Pen> </GeometryDrawi ng.Pen> <GeometryDrawi ng.Geometry> <RectangleGeometry Rect="5 5 200 240" /> </GeometryDrawi ng.Geometry> </GeometryDrawing> <!-- Сплошное перо, скрывающее половину точечной линии --> <GeometryDrawing> <GeometryDrawi ng.Pen> <Pen Brush="DodgerBlue" Thickness="5" /> </GeometryDrawi ng.Pen> <GeometryDrawing.Geometry> <RectangleGeometry Rect="2.5 2.5 205 245" /> </GeometryDrawi ng.Geometry> </GeometryDrawing> <!-- Подвеска для крепления рамки на стене --> <GeometryDrawing Geometry="M 10 0 L 105 -50 L 200 0" > <GeometryDrawi ng.Pen> <Pen Brush="Black" />
918 Глава 31. Работа с графикой </GeometryDrawing.Pen> </GeometryDrawing> </DrawingGroup> </Drawi nglmage.Drawlng> </DrawingImage> </Image.Source> </Image> </Page> Класс DrawingGroup предназначен для построения композитных изображении, которые на практике обычно составляются из объектов ImageDrawing (в основ- ном растровых изображений ) и GeometryDrawing. Объекты GeometryDrawing осно- ваны на геометрических объектах, поэтому они уже обладают определенными ко- ординатами и размерами. Растровое изображение тоже обладает метрическими размерами, определяемыми их размерами в пикселах и разрешением, однако для него также необходимо задать способ размещения относительно других гра- фических объектов. Для этой цели и предназначено свойство Rect, определяемое классом ImageDrawing. Оно не только задает определенные размеры растрового изображения, но и размещает его в двумернОлМ координатном пространстве. Класс DrawingGroup не ограничивается определением свойства Children, позво- ляющим строить кохмпозитные изображения; в нем также определяется ряд дру- гих свойств для изменения композитного изображения. Чтобы установить для композитного изображения область отсечения, задай- те свойству ClipGeometry класса DrawingGroup объект Geometry. Например, вклю- чение следующего фрагмента в PictureAndFrame.xaml отсекает угол портрета: <DrawingGroup ClipGeometry="M 0 -50 L 210 -50 L 210 120 L 0 250 Z" > Свойства Opacity и OpacityMask делают изображение полупрозрачным. Напри- мер, со следующим начальным тегом DrawingGroup все изображение становится на 50 % прозрачным: <DrawingGroup 0pacity="0.5"> Свойству OpacityMask задается объект Brush. Все цвета Brush игнорируются, а альфа-канал определяет прозрачность DrawingGroup. Следующий фрагмент включается в любую позицию между начальным и конечным тегами DrawingGroup (но не внутри элементов свойств или потомков DrawingGroup): <Drawi ngGroup.Opaci tyMask> <Rad1 a 1Gradi entBrush> <GradientStop Offset="0" Color="White" /> <GradientStop Offset="T Color="Transparent" /> </RadialGradientBrush> </Drawi ngGroup.Opaci tyMask> Рисунок остается непрозрачным в центре, но плавно растворяется ближе к краям. Свойство OpacityMask также определяется классом UIEIement, поэтому этот прием может использоваться с любыми интерфейсными элементами.
Работа с графикой 919 Еще одно свойство, определяемое UIEIement и DrawingGroup — BitmapEffect, — позволяет применять стандартные визуальные эффекты с использованием клас- сов из пространства имен System.Windows.Media.Effects. Пример: <Drawi ngGroup.Bi tmapEffect> <DropShadowBitmapEffeet /> </Drawi ngGroup.Bi tmapEffect> Этот фрагмент создает имитацию тени в правом нижнем углу рисунка. Цвет, размер и другие аспекты тени задаются свойствами DropShadowBitmapEffect. Со следующим эффектом картинка приобретает слегка «радиоактивный» вид: <DrawingGroup.Bi tmapEffect> OuterGlowBitmapEffeet GlowColor="Red" /> </Drawi ngGroup.Bi tmapEffect> Применяя следующий эффект, не беспокойтесь о своем зрении — дело вовсе не в нем: <Drawi ngGroup.Bi tmapEffect> <BlurBitmapEffeet /> </Drawi ngGroup.Bi tmapEffect> Класс DrawingGroup также определяет свойство Transform для применения пре- образований ко всему изображению — например, если картинка оказалась гораз- до крупнее, чем требовалось: <Drawi ngGroup.Т ransform> <ScaleTransform ScaleX=,,0.25" ScaleY="0.25" /> </Drawi ngGroup. T ransform> Так как Image выводит композитное изображение по центру Раде, все преоб- разования смещения игнорируются. Чтобы картина «раскачивалась» на гвозде, можно анимировать свойство RenderTransform самого объекта Image. Следующая разметка может следовать сразу же после начального тега Image: <Image.RenderT ransform> <RotateTransform x:Name="xform" /> </Image.RenderT ransform> <Image.RenderTransformOrigin> <Point X="0.5" Y="0" /> </Image.RenderT ransformOri gi n> <Image.Triggers> <EventTrigger RoutedEvent="Image.Loaded"> <BeginStoryboard> <Storyboard TargetName="xform" TargetProperty="Angle"> <DoubleAnimation Егот="-10" To="10" AutoReverse=,,True" RepeatBehavior="Forever" Accelerati onRati o="0.5" DecelerationRatio="0.5" />
920 Глава 31. Работа с графикой </Storyboard> </Beg1nStoryboard> </EventTr1gger> </Image.Tr1ggers> Наряду с изучением высокоуровневых графических классов, также будет по- лезно ознакомиться с низкоуровневыми средствами — хотя бы для того, чтобы получить представление об общих возможностях и недостатках системы. Мето- ды самого низкого уровня, которые могут использоваться в полноценных при- ложениях WPF, определены в классе DrawingContext. Как вы помните, классы, производные от UIEIement или FrameworkElement, пере- определяют метод OnRender для прорисовки интерфейсного элемента. В единст- венном параметре метода OnRender передается объект типа DrawingContext. Также можно создать объект типа DrawingVisual и воспользоваться методом RenderOpen для получения объекта DrawingContext. Далее методы класса DrawingContext ис- пользуются для выполнения графических операций; рисование завершается вы- зовом Close. В результате вы получаете объект DrawingVisual с графическим изо- бражением. Именно этот способ используется при печати и выводе графики на объектах типа RenderTargetBitmap. Если рассматривать DrawingContext как класс, определяющий все возможно- сти и ограничения графической системы WPF, он содержит на удивление мало методов графического вывода. В следующих примерах: de — объект типа Drawing- Context, brush — объект типа Brush, rect — объект типа Rect, все переменные с име- нами, начинающимися на pt, относятся к типу Point, а все переменные с имена- ми, начинающимися иа х и у, — к типу double. Конечно, DrawingContext содержит метод для рисования линии между двумя точками: dc.DrawLine(pen, ptl, pt2): DrawingContext также содержит версию DrawLine с двумя аргументами типа Animationclock: de.DrawLine(pen, ptl, animal, pt2, an1ma2); Метод OnRender может создать один или два объекта Animationclock для ани- мации двух точек. Таким образом, всего одним вызовом DrawLine метод OnRender может создать графический объект, изменяющийся со временем без дальнейше- го участия со стороны программы. Анимация в методе OnRender была продемон- стрирована в примере RenderTheAnimation предыдущей главы. Следующий метод DrawingContext рисует прямоугольник: dc.DrawRectangle(brush, pen, rect): Структура Rect определяет местоположение левого верхнего угла прямо- угольника, его ширину и высоту. Аргументы Brush и Реп можно задать равны- ми null, чтобы отменить прорисовку внутренней области или периметра. Также существует версия DrawRectangle с аргументом Animationclock, анимирующим объект Rect.
Работа с графикой 921 Метод DrawRoundedRectangle рисует прямоугольник с закругленными краями. Крутизна закругления определяется двумя последними аргументами: de.DrawRoundedRectangle(brush, pen, rect, /Radius, yRadius); Версия DrawRoundedRectangle с тремя аргументами Animationclock анимирует прямоугольник или любые из полуосей закругления. Метод DrawRoundedRectangle может использоваться для рисования эллипса — для этого последние два аргумента вычисляются на основании Rect: dc.DrawRoundedRectangle(brush, pen. rect, rect.Width / 2, rect.Height / 2): Впрочем, той же цели можно добиться методом DrawEllipse: de.DrawEl1ipse(brush, pen, ptCenter, /Radius, yRadius): Вторая версия DrawEllipse содержит три аргумента Animationclock для центра и двух полуосей. Метод DrawText при вызове получает аргумент типа FormattedText: de.DrawTe/t(formtxt. ptOrigin); Для следующего метода DrawingContext требуется объект GlyphRun — серия символов с точно заданными размещениями: dc.DrawGlyphRun(brush, glyphrun); У метода DrawVideo в аргументе media передается экземпляр MediaPlayer — класс, способный воспроизводить видео- и аудиоролики по заданному URI ло- кального или удаленного файла: dc.DrawVideo(media, rect): Последние три метода вывода предназначены для общих операций с вектор- ной и растровой графикой и их комбинациями. Чтобы вывести на экране объект Geometry, необходимо передать в вызове методы Brush и Реп при вызове метода DrawingContext: dc.DrawGeometry(brush, pen, geometry); Метод Drawlmage выводит растровое изображение. Его первый аргумент от- носится к типу ImageSource, который, как говорилось ранее, также включает класс Drawingimage, класс BitmapSource и его потомков: dc.DrawImage(imgsrc, rect); Последний метод вывода получает аргумент типа Drawing: de. DrawDrawi ng (drawl ng); Класс DrawingContext также включает методы для задания отсечения, уровня непрозрачности и выполнения преобразований. Все эти методы реализованы в виде стековых операций, и занесенный в стек атрибут применяется ко всем будущим графическим операциям до тех пор, пока вызов Pop ие извлечет его из стека. Метод PushClip с аргументом типа Geometry задает область отсечения.
922 Глава 31. Работа с графикой Метод PushOpacity получает аргумент типа double со значениями от 0 (полная прозрачность) до 1 (полная непрозрачность), а вторая версия PushOpacity получа- ет аргумент Animationclock. Метод PushTransform получает аргумент типа Transform. Насколько важную роль играет Drawing в общей схеме графики WPF? При создании объекта DrawingVisual и последующем вызове RenderOpen для получения DrawingContext объект DrawingVisual сохраняет результаты всех графических опе- раций в доступном только для чтения свойстве Drawing, возвращающем объект типа DrawingGroup. Тема кистей уже была затронута во второй главе книги, но в этой главе были рассмотрены не все типы кистей. Настало время познакомиться с ними побли- же. Полная иерархия класса Brush выглядит так: Object Dispatcherobject (абстрактный) Dependencyobject Freezable (абстрактный) Animatable (абстрактный) Brush (абстрактный) GradientBrush (абстрактный) LinearGradientBrush RadialGradientBrush SolidColorBrush TileBrush (абстрактный) DrawingBrush ImageBrush VisualBrush Обратите внимание на три класса, производных от TileBrush. Эти кисти созда- ются на базе других графических объектов. Класс DrawingBrush обладает свойством Drawing. Чаще его значение представляет собой объект GeometryDrawing (то есть кисть будет использоваться для вывода векторной графики), но значение Drawing также может включать растровую графику и объекты MediaPlayer. Класс ImageBrush содержит свойство ImageSource. Обычно ему задается объект Bitmapimage, но в его значении также может использоваться объект Drawingimage и различные графические объекты. Класс VisualBrush обладает свойством Visual, которому задается объект DrawingVisual, созданный посредством вывода на DrawingContext. Впрочем, следует напомнить, что классы UIEIement и FrameworkElement тоже на- следуют от Visual. Кисти VisualBrush часто применяются для отображения эле- ментов управления и других компонентов пользовательского интерфейса — на- пример, теней и отражений. Три типа кистей, производных от TileBrush, работают по одинаковым принци- пам. Поверхность «покрывается» объектом Drawing (или Image, или Visual), при- чем вы можете управлять многими параметрами растяжения или заполнения поверхности «плитками». Для этой цели используются восемь свойств, опреде- ляемых TileBrush и наследуемых тремя классами кистей. От умения пользовать- ся этими восемью свойствами TileBrush напрямую зависит то, насколько эффек- тивной будет использование этих трех типов кистей.
Работа с графикой 923 В следующем простом примере использования ImageBrush восьми свойствам TileBrush оставлены значения по умолчанию, а свойству ImageSource задана кисть, сформированная на основе растрового изображения с моего веб-сайта. Кисть на- значается свойству Background элемента Раде. SimplelmageBrush.xaml <।_ _ =================================================== SimplelmageBrush.xaml (с) 2006 by Charles Petzold <Page xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Page.Background> <ImageBrush ImageSource="http://www.charlespetzold.com/PetzoldTattoo.jpg"> </ImageBrush> </Page.Background> </Page> Запустив эту программу в XAML Cruncher, вы увидите, что растровое изо- бражение растягивается с заполнением всей страницы. Если пропорции страни- цы не соответствуют пропорциям рисунка, изображение искажается. Это пове- дение отличается от того, которое используется Image по умолчанию, но оно больше подходит для применения растрового изображения в качестве кисти. Ри- сунок должен занимать всю область, накрываемую кистью, — в данном случае это весь объект Раде. Далее приводится простой пример VisualBrush с такой же общей структурой, как и у примера SimplelmageBrush.xaml. SimpleVisualBrush.xaml <I_ _ ==================================================== SimpleVisualBrush.xaml (c) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Page.Background> <VisualBrush> <VisualBrush.Vi sual> <Button Content="Button?" /> </VisualBrush.Visual> </VisualBrush> </Page.Background> </Page> Свойство Visual элемента VisualBrush выделяется в элемент свойства. Ему за- дается элемент Button, в результате чего кисть начинает выглядеть именно так — как большая кнопка. Впрочем, эта «псевдокнопка» не реагирует на мышь и кла- виатуру. Это не более чем визуальное изображение кнопки, растянутое для
924 Глава 31. Работа с графикой заполнения всего фона страницы без учета исходных пропорций. Если вы соби- раетесь использовать изображение кнопки в качестве кисти, такое поведение по умолчанию вполне разумно. Для завершения картины приведу простой пример использования DrawingBrush. SimpleDrawingBrush.xaml <।_ _ ===================================================== SimpleDrawingBrush.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x=”http://schemas.microsoft.com/wi nfx/2006/xaml "> <Page.Background> <DrawingBrush> <Drawi ngBrush.Drawlng> <GeometryDrawl ng Brush="Red"> <GeometryDrawl ng.Pen> <Pen Brush="Blue" /> </GeometryDrawi ng.Pen> <GeometryDrawing.Geometry> <EllipseGeometry Center="0.0" RadiusX="10" RadiusY="10" /> </GeometryDrawing.Geometry> </GeometryDrawing> </Drawi ngBrush.Drawlng> </DrawingBrush> </Page.Background> </Page> Рисуется эллипс с центром в точке (0,0), синим контуром и красным запол- нением. Эллипс растягивается для заполнения площади страницы с пропорцио- нальным увеличением ширины синего пера. Из-за малого размера полуосей эллипса (10 единиц) синяя линия контура, толщина которой по умолчанию со- ставляет всего один пиксел, становится довольно толстой. А вот пример более сложного объекта DrawingBrush; по внешнему виду он на- поминает двух рыб, синюю и розовую, плывущих в разные стороны. FishBrush.xaml <।_ _ ============================================ FishBrush.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Page.Background> <DrawingBrush> <Drawi ngBrush.Drawlng> <DrawingGroup> <!-- Заполнение фона розовым цветом --> <GeometryDrawing Brush="Pink">
Работа с графикой 925 <GeometryDraw i ng. Geometry> <RectangleGeometry Rect="0 0 200 100" /> </GeometryDraw1 ng.Geometry> </GeometryDraw1ng> <Geometry Dr awing Brush="Aqua"> <Geomet ryDrawi ng.Pen> <Pen Brush="Blue" Th1ckness="2" /> </GeometryDraw1ng.Pen> <GeometryDraw1ng.Geometry> <GeometryGroup> <!-- Контур синей рыбы --> <PathGeometry> <PathF1gure StartPo1nt="200 0" IsClosed="True" IsFilled="True”> <BezierSegment Pointl="150 100" Point2="50 -50" Point3="0 50" /> <BezierSegment Pointl="50 150" Point2="150 0" Po1nt3="200 100" /> </PathFigure> </PathGeometry> <!-- Глаза рыб --> <Е111pseGeometry Center="35 35" RadiusX-"5" RadiusY="5" /> <E111pseGeometry Center="165 85" Rad1usX="5" Rad1usY="5" /> </GeometryGroup> </GeometryDraw1ng.Geometry> </GeometryDraw1ng> </Draw1ngGroup> </Draw1ngBrush.Drawlng> </Draw1ngBrush> </Page.Background> </Page> Уровень вложенности элементов в этом коде XAML весьма глубок, но общая структура остается такой же, как в предыдущих примерах. Файл определяет эле- мент Раде со свойством Background, которому задан объект DrawingBrush. Свойст- ву Drawing элемента DrawingBrush задается объект DrawingGroug, содержащий два объекта GeometryDrawing. Первый объект определяет прямоугольник размером 200x100 единиц. Второй объект содержит элемент PathGeometry, рисующий кон- тур рыбы, и два элемента EllipseGeometry для рисования глаз. При запуске программы в XAMR Cruncher фигура заполняет страницу. В цен- тре рисуется синяя рыба, плывущая влево. Внизу мы видим розовую рыбу, плы- вущую вправо, а наверху — другую розовую рыбу, плывущую вправо.
926 Глава 31. Работа с графикой По координатам различных объектов Geometry в FishBrush.xaml легко опреде- лить, что фигура имеет 200 единиц в ширину и 100 единиц в высоту. Но в ре- зультате физических размеров пера и значения по умолчанию Miter свойства LineJoin объект Drawing фактически получается чуть больше этих размеров. Для определения фактических размеров объекта Drawing можно присвоить элементу DrawingGroup имя: <DrawingGroup х: Nauie="drawgrp"> Затем перед закрывающим тегом Раде вставляется следующий элемент Label: <Label Content="{Binding ElementName=drawgrp, Path=Bounds}" /> Как выясняется, рисунок имеет 202 единицы в ширину и 108 единиц в высо- ту. В XAML Cruncher виден белый периметр вокруг розового прямоугольника. Чтобы значительно сократить его, задайте свойству LineJoin пера значение Bevel: <Pen Brush="Blue" Thickness="2" L1neJoin="Bever /> Можно немного повозиться с координатами, чтобы объект Drawing действи- тельно имел 200 единиц в ширину и 100 единиц в высоту, но не стоит тратить время. Белый периметр раздражает, однако вскоре вы узнаете, как убрать его. Итак, у нас имеются XAML-файлы с объектами ImageBrush, VisualBrush и Draw- ingBrush. Каждая из этих кистей основана на объекте, обладающим определен- ными метрическим размерами. Для растрового изображения в ImageBrush метри- ческий размер вычисляется делением размеров растра в пикселах на разрешение в точках на дюйм. Для Button в VisualBrush метрический размер равен размеру Button, определяемому его методом MeasureOverride. Для объекта Drawing в Draw- ingBrush метрический размер можно получить из свойства Bounds объекта Drawing. Во всех случаях метрический размер и пропорции объекта игнорируются, и объ- ект растягивается дл,ч заполнения области, накрываемой кистью. Давайте немного поэкспериментируем. Все свойства, которые я буду описы- вать при обсуждении кистей, определяются классом TileBrush, поэтому они мо- гут задаваться в элементах ImageBrush, VisualBrush и DrawingBrush четырех XAML- файлов, использующих эти кисти. Режим растяжения по умолчанию определяется свойством Stretch, опреде- ляемым TileBrush, которому задается один из элементов перечисления Stretch. По умолчанию используется режим Stretch.Fill, при котором выполняется растяже- ние без учета пропорций. В любом из представленных XAML-файлов также можно опробовать и другие режимы Stretch. Попробуйте внести следующее изменение в SimplelmageBrush.xaml: <ImageBrush Stretch="Uniform" ... Или такое — в SimpleVisualBrush.xaml: <Vj sualBrush Stretch="lln1 form"> Или следующее в SimpleDrawingBrush.xaml или FishBrush.xaml: <DrawingBrush Stretch="Uniform">
Работа с графикой 927 Во всех случаях изображение (визуальный объект, рисунок) сохраняет пра- вильные пропорции. Кисть при этом по-прежнему накрывает весь фон страницы, а изображение на кисти — нет. Растровый рисунок, кнопка или графика запол- няют фон только по одной из двух осей. По другой оси кисть остается прозрач- ной по обеим сторонам от рисунка. Если выбран режим Stretch.Uniform, а изображение занимает всю высоту стра- ницы, его можно сдвигать в левую или правую часть кисти; для этого свойству AlignmentX, определяемому TileBrush, задаются элементы Left или Right перечисле- ния AlignmentX. По умолчанию используется режим Center. Аналогично, если изо- бражение занимает полную ширину страницы, его можно сдвигать вверх или вниз, задавая свойству AlignmentY значение Тор пли Bottom. В следующем режиме Stretch — UniformFill — изображение сохраняет исходные пропорции, но всегда заполняет весь фон с применением отсечения. По умолча- нию изображение выравнивается по центру кисти. Если изображение вписы- вается в высоту страницы, а левая и правая части отсекаются, можно задать AlignmentX равным Left, чтобы усекалась только правая часть. Если же отсекают- ся полосы у верхнего и нижнего края изображения, то при задании свойства AlighmentY равным Тор усекается только нижняя часть изображения. В последнем режиме Stretch — None — изображение (визуальный объект, рису- нок) сохраняет свои метрические размеры и размещается в центре кисти. Чтобы сместить его к одной из сторон или углов, воспользуйтесь комбинацией Align- mentX и AlignmentY. Конечно, одной из важнейших возможностей классов, производных от TileBrush, является мозаичный режим. Другими словами, изображение (визуальный объ- ект, рисунок) многократно дублируется по горизонтали и вертикали. Для управ- ления мозаичным режимом используется свойство TileMode, которому задается один из элементов одноименного перечисления. По умолчанию свойство TileMode равно TileMode.None. Для включения мозаичного режима свойству TileMode обыч- но задается значение TileMode.Tile. Впрочем, сам по себе выбор режима TileMode.Tile еще ничего не делает. По умол- чанию считается, что размер каждой «плитки» равен размеру кисти; таким обра- зом, единственная плитка заполняет всю страницу. Чтобы фон был вымощен несколькими «плитками», необходимо задать значения свойств Viewport и View- portUnits. Но сначала нужно принять некоторые решения: должно ли количество плиток быть фиксированным? Или вы предпочитаете зафиксировать метриче- ский размер плиток? Допустим, создаваемая кисть ImageBrush должна состоять из 10 плиток по горизонтали и 5 плиток по вертикали, независимо от размера кисти. Как след- ствие, ширина каждой плитки должна составлять 1/10 общей ширины кисти, а высота — 1/5 (то есть 20 %) от общей высоты кисти. Эти размеры задаются при помощи свойства Viewport: <ImageBrush TileMode="Tile" V1ewport=”0 0 0.1 0.2" ... По умолчанию система координат, связанная со свойством Viewport, базирует- ся на общем размере кисти. Свойство Viewport относится к типу Rect, но свойства
928 Глава 31. Работа с графикой Left и Right (первые два числа в строке, задаваемой Viewport) игнорируются. Свой- ства Width и Height определяют ширину и высоту плитки в отношении ширины и высоты кисти. По умолчанию свойство Viewport эквивалентно строке "О 0 1 1", то есть плитка создается с тем же размером, что и кисть. Например, создание в примере VisualBrush матрицы 10x10 кнопок происхо- дит так: <V1sualBrush T11eMode="Tile” V1ewport="0 0 0.1 0.1”> В примере FishBrush.xaml следующая разметка создает матрицу из 25 плиток по горизонтали и 33,33 плитки по вертикали: <DrawingBrush TileMode="T11e" Viewport="0 0 0.04 0.03"> Белое пространство вокруг каждой плитки выглядит неприятно и нарушает мозаичный эффект... но будьте терпеливы. При использовании Viewport в режиме по умолчанию размер каждой плитки определяется на основании размера кисти и размеров Viewport, поэтому пропор- ции изображений на плитках (в общем случае) искажаются. Если вы хотите зафиксировать количество плиток по горизонтали и вертикали, но при этом со- хранить пропорции исходного изображения, задайте свойству Stretch значение Uniform (в этом случае по сторонам появляется пустое пространство) или Uni- formToFill (часть изображения будет отсечена). Но не задавайте Stretch равным None, потому что тогда изображение будет выводиться в своих метрических раз- мерах. Скорее всего, плитка будет меньше «нерастянутого» изображения и по- этому будет содержать только небольшой фрагмент. Чтобы каждая плитка имела фиксированный размер, задайте его пропорцио- нальным метрическому размеру изображения. В свойстве Viewport можно ис- пользовать аппаратно-независимые единицы, если задать Viewportunits значе- ние BrushMappingMode.Absolute (по умолчанию используется значение BrushMapping- Mode.RelativeToBoundingBox). Это же перечисление BrushMappingMode используется для задания свойства MappingMode, определяемого GradientBrush. Изображение, выводимое файлом SimplelmageBrush.xaml, обладает пропорция- ми приблизительно 1:1,19, поэтому следующая разметка задает каждой плитке ширину около 0,5 дюйма и высоту на 19 % больше: <ImageBrush TileMode="Tile” Viewport="0 0 50 60" ViewportUnits="Absolute" ... Если свойству Viewportunits задано значение Absolute, первые два числа в стро- ке Viewport влияют на отображение плитки в левом верхнем углу кисти. Если они равны (0,0), то левый верхний угол левой верхней плитки размещается в ле- вом верхнем углу кисти. Но попробуйте использовать значения 25 и 30: <ImageBrush TileMode="Tile" Viewport="25 30 50 60" ViewportUnits="Absolute" ... Плитки остаются с прежним размером, но вся матрица плиток сдвигается на 25 единиц влево и 30 единиц вверх, поэтому в левом верхнем углу кисти видна только правая нижняя четверть плитки.
Работа с графикой 929 Рисунок, выводимый файлом FishBrush.xaml, имеет пропорции 2:1, поэтому задать размеры плиток несложно: <DrawingBrush Т11 eF1ode=”Ti 1 е” V1ewport="0 0 60 30" ViewportUnits="Absolute"> Класс TileBrush определяет два свойства Viewbox и Viewboxllnits, которые явля- ются аналогами Viewport и Viewportunits. ViewBox задает прямоугольник в изобра- жении (визуальном объекте, рисунке); по умолчанию он задается в координатах относительно ограничивающего прямоугольника. Фрагмент разметки Simple- lmageBrush.xaml: <ImageBrush TileMode="Tile" V1ewport=”0 0 50 60" ViewportUnits="Absolute" Viewbox="0.3 0.05 0.4 0.6" ... В данном случае для мозаичного размещения используется только 40 % ши- рины растрового изображения, начиная с 30 % от левой стороны. Используется только 60 % высоты изображения, начиная в 5 % от верха. Первые два числа мо- гут быть отрицательными, а два других могут быть больше 1; в обоих случаях это приводит к созданию прозрачных областей вокруг изображения. Использование свойства Viewbox в режиме Absolute позволяет избежать в при- мере FishBrush.xaml появления вокруг плиток пустых областей, обусловленных конечной шириной пера: <DrawingBrush TileMode="Tile" V1ewport="0 0 60 30" ViewportUnits="Absolute" Viewbox="0 0 200 100" ViewboxUnits="Absolute"> Элементы None и Tile перечисления TileMode уже рассматривались ранее. Кро- ме них, перечисление содержит еще три элемента. В режиме FlipX столбцы заме- няются своими «зеркальными отражениями» вокруг вертикальной оси. В режи- ме FlipY каждая строка плиток «отражается» вокруг горизонтальной оси. Режим FlipXY сочетает эти два эффекта. Иногда совершенно нормальное изображение превращается в почти абстрактную конструкцию — попробуйте использовать FlipXY в примере SimplelmageBrush.xaml: <ImageBrush T11eMode="FljpXY" Viewport="0 0 50 60" Viewportllnits="Absolute" Viewbox="0.3 0.10 0.4 0.6" ... Одно из интересных применений VisualBrush — украшение пользовательского интерфейса тенями или отражениями элементов управления/интерфейсных эле- ментов. Следующий автономный XAML-файл создает панель StackPanel с двумя элементами TextBlock и тремя элементами CheckBox. Фон другой панели StackPanel окрашивается кистью VisualBrush, имитирующей отражения этих элементов. Свой- ству Visual объекта VisualBrush задается привязка к панели StackPanel, содержащей элементы. Маска OpacityMask, основанная на LinearGradientBrush, обеспечивает по- степенное растворение изображения у нижнего края. ReflectedControls.xaml <।_ _ ==================================================== ReflectedControls.xaml (с) 2006 by Charles Petzold
930 Глава 31. Работа с графикой <Page xmlns="http://schemas.mlcrosoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Page.Resources> <Style TargetType="{x:Type TextBlock}"> <Setter Property="\/erticalAlignment" Value="Bottom" /> <Setter Property=”FontFamily” Value="Lucida Calligraphy" /> <Setter Property="FontSize" Value="36" /> </Style> <Style TargetType="{x:Type CheckBox}"> <Setter Property="FontSize" Value="24" /> <Setter Property="Margin" Value="12" /> </Style> </Page.Resources> <!-- Панели StackPanel для элементов и их отражений --> <StackPanel> <!-- Панель StackPanel для элементов --> <StackPanel Name="pnlControls" Orientation="Horizontal" HorlzontalAlignment="Center"> <TextBlock Text="Check..." /> <StackPanel HorlzontalAljgnment="Center"> <CheckBox Content="CheckBox 1" /> <CheckBox Content="CheckBox 2" /> <CheckBox Content="CheckBox 3" /> </StackPanel> <TextBlock Text="...Boxes" Л </StackPanel> <!-- Панель StackPanel для отражений --> <StackPanel Hei ght="{Binding ElementName=pnlControls. Path=Actна 1Height}"> <!-- VisualBrush переворачивает изображение элементов --> <StackPanel.Background> <VisualBrush Visual="{Binding ElementName=pnlControls}" Stretch="None"> <Vi sualBrush.Relati veT ransform> <TransformGroup> <ScaleTransform ScaleX=’T’ ScaleY="-l" /> <TranslateTransform Y="l" /> </TransformGroup> </Vi sualBrush.Relati veT ransform> </VisualBrush> </StackPanel.Background> <!-- Маска OpacityMask постепенно растворяет изображение--> <StackPanel.OpacityMask>
Работа с графикой 931 <LinearGradientBrush StartPoint="0 0" EndPoint=”0 Г> <GradientStop Offset="0" Color="#80000000" /> <GradientStop Offset=’T’ Color="#00000000" /> </LinearGradientBrush> </StackPanel.OpacityMask> </StackPanel> </StackPanel> </Page> Хотя «отражения» CheckBox в VisualBrush не работают, их внешний вид дина- мически изменяется с изменением состояния «настоящих» кнопок CheckBox. К объекту Geometry, заложенному в основу DrawingBrush, можно применять анимацию, хотя после следующего примера вы, возможно, решите, что делать этого не стоит. AnimatedDrawingBrush.xaml <!_ _ ======================================================= AnimatedDrawingBrush.xaml (с) 2006 by Charles Petzold <Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml"> <Page.Background> <DrawingBrush TileMode="Tile" Stretch="None" Viewport="0 0 12 12" ViewportUnits="Absolute"> <Drawi ngBrush.Drawlng> <GeometryDrawing Brush="Blue"> <GeometryDrawi ng.Geometry> <EllipseGeometry x:Name="elipsgeo" Center="0 0" /> </GeometryDrawi ng.Geometry> </GeometryDrawing> </Drawi ngBrush.Drawlng> </DrawingBrush> </Page.Background> <Page.Triggers> <EventTrigger RoutedEvent="Page.Loaded"> <BeginStoryboard> <Storyboard TargetName="elipsgeo" RepeatBehavior="Forever"> <DoubleAnimation Storyboard.TargetProperty="RadiusX" From="4" To="6" Duration="0:0:0.25" AutoReverse="True" /> <DoubleAnimation Storyboard.TargetProperty="RadiusY" From="6" To="4" Duration="0:0:0.25" AutoReverse="True" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Page.Triggers> </Page>
932 Глава 31. Работа с графикой Последняя программа в книге анимирует не DrawingBrush, а элемент Ellipse, за- полненный кистью DrawingBrush. Я назвал эту программу HybridClock, и она компилируется в формате ХВАР. Программа рисует круглый циферблат, как у обычных аналоговых часов, но только с одной стрелкой, которая перемещает- ся по экрану, отсчитывая секунды. При этом сама стрелка представляет собой своего рода цифровые часы: на нсчй выводится текстовая строка с текущей датой и временем. Формат даты и времени выбирается в контекстном меню. Следующий класс предоставляет часам текущую дату и время, отформатиро- ванные в виде строки. Класс реализует интерфейс INotifyPropertyChanged и предо- ставляет событие PropertyChanged для свойства DateTime, поэтому он может ис- пользоваться в качестве источника для привязки данных. Именно так происходит обновление даты и времени, отображаемых на стрелке часов. ClockTicker.cs //--------------------------------------------- // ClockTicker.cs (с) 2006 by Charles Petzold //--------------------------------------------- using System: using System.ComponentModel; usi ng System.Wi ndows.Thread!ng; namespace Petzold.HybridClock { public class ClockTicker : INotifyPropertyChanged { string strFormat = "F": // Необходимое событие интерфейса public event PropertyChangedEventHandler PropertyChanged: // Открытое свойство public string DateTime { get { return System.DateTime.Now.ToString(strFormat); } } public string Format { set { strFormat = value: } get { return strFormat: } // Конструктор public ClockTicker() { DispatcherTimer timer = new DispatcherTimer(): timer.Tick += TimerOnTick: timer.Interval = TimeSpan.FromSeconds(0.10);
Работа с графикой 933 timer. StartO: } // Обработчик события таймера инициирует событие PropertyChanged void TimerOnTick(object sender, EventArgs args) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("DateTime")): } } Проект HybridClock включает маленький файл определения приложения, со- держащий ссылку на единственный XAML-файл этого проекта. HybridClockApp.xaml <।_ _ ================================================= HybridClockApp.xaml (с) 2006 by Charles Petzold <Application x:Class="Petzold.HybridClock.HybridClockApp" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml ns:x="http://schemas.microsoft.com/wi nfx/2006/xaml" StartupUri="HybridClockPage.xaml" /> Размышляя над тем, как должны работать часы, я знал, что изображение долж- но размещаться в поле Viewbox, чтобы оно увеличивалось и уменьшалось с изме- нением размеров окна. Но при этом было понятно, что текстовая строка, обра- зующая единственную стрелку часов, создаст проблемы с масштабированием. Если пользователь выберет формат даты и времени с названием месяца, длина строки будет изменяться со сменой месяца. Кроме того, длина строки будет изменяться при переходе от 12:59 к 1:00 или при переходе даты с 9-го на 10-е число. Я хотел, чтобы XAML-файл каким-то образом компенсировал эти изменения. Часы содержат целых восемь элементов TextBlock, на которых отображается одна и та же строка даты и времени. Первый из этих восьми элементов TextBlock содержит привязку к объекту ClockTicker; второй содержит привязку к свойству Text первого элемента TextBlock. Каждый элемент TextBlock существует в паре с другим элементом. Одна пара задает горизонтальный размер, а другая — вертикальный размер циферблата. Эти четыре элемента TextBlock размещаются на прозрачной панели. Две другие пары синхронно вращаются вокруг своих центров. В любой момент времени ви- ден только один элемент TextBlock. Когда стрелка направлена вертикально вверх или вниз, в результате анимации один элемент «растворяется», а другой «про- является», благодаря чему текст никогда не выводится в перевернутом виде. HybridClockPage.xaml <! _ _ ================================================= HybridClockPage.xaml (с) 2006 by Charles Petzold
934 Глава 31. Работа с графикой <Page х:Class="Petzold.HybridClock.HybridClockPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml /presentatlon" xml ns:x="http://schemas.microsoft.com/winfx/2006/xaml" xml ns:src="clr-namespace:Petzold.HybridClock" WindowTitle="Hybrid Analog/Digitai Clock" Title="Hybrid Ana 1og/Digitai Clock" Background="Pink"> <!-- Ресурс для класса, предоставляющего текущее время --> <Page.Resources> <src:ClockTicker x:Key="clock" /> </Page.Resources> <!-- Экранная подсказка с информацией об авторских правах --> <Page.ToolTiр> TextBlock TextAlignment="Center"> Hybrid Analog/Digital Clock <LineBreak />&#x00A9; 2006 by Charles Petzold <LineBreak />www.charlespetzold.com </TextBlock> </Page.ToolTip> <Viewbox> <!-- Внешняя сетка из одной ячейки вмещает весь циферблат --> <Grid> <Е11ipse> <Е11ipse.Fi11> <SolidColorBrush Color="{x:Static src:HybridClockPage.clrBackground}" /> <!-- К сожалению, применение радиальной кисти ухудшает быстродействие --> <!-- Radi alGradientBrush Gradient0rigin="0.4, 0.4"> <Radial GradientBrush.GradientStops> <GradientStop 0ffset="0" Color="White" /> <GradientStop Offset='T" Color="{x:Static src:HybridClockPage.clrBackground}" /> </RadialGradientBrush.GradientStops> </RadialGradientBrush --> </Е11ipse.Fi11> </Е11ipse> <!-- Внутренняя сетка из одной ячейки, размеры которой определяются по ширине текста --> <Grid Name="grd" Margin="12"> <!-- Создание двух невидимых горизонтальных строк с датой/временем -->
Работа с графикой 935 <StackPanel 0r1entat1on="Hor1zontal" 0pac1ty="0" VerticalAl jgnment='’Center "> <TextBlock Name="datet1me" Text="{B1nd1ng Source={Stat1cResource clock}, Path=DateT1me}" /> <TextBlock Text="{Bind1ng ElementName=datet1me, Path=Text}" /> </StackPanel> <!-- Создание двух невидимых вертикальных строк с датой/временем --> <StackPanel 0rjentat1on="Horjzontal" 0pac1ty="0" Hori zontalAl1gnment="Center"> <TextBlock Text="{B1nding ElementName=datet1nie, Path=Text}" /> <TextBlock Text="{B1nd1ng ElementName=datet1meI Path=Text}" /> <StackPanel.LayoutTransform> <RotateTransform Angle="90" /> </StackPanel,LayoutTransform> </StackPanel> <!-- Создание двух вращающихся строк с датой/временем --> <StackPanel 0r1entat1on="Horizontal" HorizontalA11gnnient="Center" VerticalAlignment="Center" StackPanel,RenderTransform0r1g1n="0.5 0.5"> <TextBlock Text="{B1nd1ng ElementNanie=datetime, Path=Text}" Opacity="0" /> <TextBlock Text="{B1nd1ng ElementName=datet1nie1 Path=Text}" Name="txtl" Opacity=,,0.5"/> <StackPanel.RenderTransform> <RotateTransform x:Name="xforml"/> </StackPanel,RenderTransform> </StackPanel> <!-- Две другие вращающиеся строки с датой/временем --> <StackPanel 0r1entat1on="Hor1zontal" HorizontalAl 1gnment="Center" Vert1ca1 Al 1gnment="Center" RenderTransform0r1g1n="0.5 0.5"> <TextBlock Text="{B1nd1ng ElementName=datet1nie, Path=Text}" Name="txt2" Opacity="0.5" /> <TextBlock Text="{B1nd1ng ElementName=datet1nie, Path=Text}" 0pac1ty="0" />
936 Глава 31. Работа с графикой <StackPanel.RenderTransform> <RotateTransforn x:Nane="xforn2"/> </StackPanel.RenderTransforn> </StackPanel> </Gr1d> <!-- Вращающаяся маска (определяется в программном коде) --> <Е11Ipse Nane="nask" RenderTransforn0r1g1n="0.5 0.5" > <Е 111pse.RenderT ransform> <RotateTransform x:Nane="xforn3,7> </El11pse.RenderTransforn> </El11pse> </Gr1d> </V1ewbox> <Page.Tr1ggers> <EventTr1gger RoutedEvent="Page.Loaded"> <Beg1nStoryboard> <Storyboard x:Nane="storyboard"> <!-- Двойная анимация поворачивает текст и маску прозрачности --> <DoubleAnination Storyboard.TargetNane="xfornr Storyboard.TargetProperty="Angle" Fron="-90" To="270" Durat1on=,,0:l:0" RepeatBehav1or="Forever" /> <DoubleAnination Storyboard.TargetNane="xforn2" Storyboard.TargetProperty="Angle" Fron="-270" To="90" Durat1on="0:l:0" RepeatBehav1or=,,Forever" /> <DoubleAnination Storyboard.TargetNane="xforn3" Storyboard.TargetProperty="Angle" Fron="-90" To="270" Durat1on="0:1:0" RepeatBehav1or="Forever" /> <!-- Анимация по ключевым кадрам обеспечивает растворение --> <Dou b1eAninatlonUsIngKeyFranes Storyboard.TargetNane="txtl" Storyboard.TargetProperty="0pac1ty" Durat1on="0:1:0" RepeatBehav1or="Forever"> <L1nearDoubleKeyFrane Value=,,l" KeyT1ne="0:0:0.5" /> <D1screteDoubleKeyFrane Value=,,l" KeyT1ne="0:0:29.5" /> <L1nearDoubleKeyFrane Value="0" KeyT1ne="0:0:30.5" /> <D1screteDoubleKeyFrane Value="0" KeyT1ne="0:0:59.5" /> <L1nearDoubleKeyFrane Value="0.5" KeyT1ne="0:l:0" /> </DoubleAn1nat1onUsIngKeyFrames>
Работа с графикой 937 <DoubleAnlmat1onUsingKeyFrames Storyboard.TargetName="txt2" Storyboard.TargetProperty="0pac1ty" Durat1on="0:1:0" RepeatBehavior="Forever"> <L1nearDoubleKeyFrame Value="0" KeyT1me="0:0:0.5" /> <D1screteDoubleKeyFrame Value="0" KeyT1me="0:0:29.5" /> <L1nearDoubleKeyFrame Value=,T' KeyT1me="0:0:30.5" /> <D1screteDoubleKeyFrame Value="l" KeyT1me="0:0:59.5" /> <L1nearDoubleKeyFrame Value="0.5" KeyT1me="0:l:0" /> </DoubleAnima 11onUsingKeyF rames> </Storyboard> </Beg1nStoryboard> </EventTr1gger> </Page.Tr1ggers> </Page> Поскольку размер циферблата определяется длиной текста, отображаемого на стрелке часов, любые изменения в длине текста (например, при переходе к дру- гому месяцу) приводят к изменению размера циферблата. Однако циферблат находится в поле Viewbox, a Viewbox немедленно изменяет свое содержимое так, что оно визуально сохраняет исходный размер. Висите все выглядит так, будто изменяется размер шрифта по отношению к размерам циферблата. Изменения фактического размера циферблата не позволили мне включить часовые деления в XAML-файл. Я решил реализовать их в программном коде и изменять при изменении длины текстовой строки. В код XAML также был включен вращающийся эллине (анимируемый третьим объектом DoubleAnimation), заполненный кистью с переменной прозрачностью. В результате применения кисти деления на циферблате «проступают» при приближении к ним стрелки, а затем постепенно исчезают из вида. При изменении длины текста кисть при- ходится строить заново в программном коде. Далее приводится кодовая часть проекта HybridClock. Помимо вывода деле- ний и построения полупрозрачного эллипса, код также обеспечивает отображе- ние контекстного меню. HybridClockPage.xaml.cs //------------------------------------------------------ // HybridClockPage.zxaml.cs (с) 2006 by Charles Petzold //------------------------------------------------------ using System; using System.Windows: using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging: using System.Windows.Shapes: namespace Petzold.HybridClock
938 Глава 31. Работа с графикой { public partial class HybridClockPage : Page { // Инициализация цветов для XAML public static readonly Color clrBackground = Colors.Aqua: public static readonly Brush brushBackground = Brushes.Aqua: // Преобразования для делений на циферблате TranslateTransform[] xform = new TranslateTransform[60]: public HybrldClockPageO { Initial IzeComponentO; // Задание свойства BeginTime элемента Storyboard storyboard.BeglnTIme = -DateTime.Now.TlmeOfDay; // Назначение обработчика для события Opened // контекстного меню ContextMenu menu = new ContextMenu(): menu.Opened += ContextMenuOnOpened: ContextMenu = menu: // Назначение обработчика для события Loaded Loaded += WlndowOnLoaded; } void WlndowOnLoaded(object sender, EventArgs args) { // Создание делений на циферблате часов for (Int 1 = 0: 1 < 60: 1++) { Ellipse ellps = new EllipseO: ellps.HorizontalAlignment = HorizontalAlignment.Center: el Ips.Vertical AlIgnment = VerticalAlignment.Center: ellps.Fill = Brushes.Blue: el Ips.Width = ellps.Height = 1 % 5 == 0 ? 6 : 2: TransformGroup group = new TransformGroup(): group.Children.Add(xform[1] = new TranslateTransform(datet1me.ActualW1dth, 0)): group.Children.Add( new TranslateTransform(grd.Marg1n.Left / 2, 0)): group.Chi 1dren.Add( new TranslateTransformOellps.Width / 2, -ellps.Height /2)); group.Children.Add( new RotateTransformd * 6)):
Работа с графикой 939 group.Chi 1dren.Add( new TranslateTransform(el1ps.W1dth / 2, elips.Height /2)): el Ips.RenderTransform = group; grd.Chi 1dren.Add(el 1ps); } // Построение маски прозрачности MakeMask(); // Назначение обработчика события для изменения размера // строки даты/времени datetime.SIzeChanged += DateTImeOnSIzeChanged; } // Строка даты/времени изменилась; // пересчитать преобразования и маску void DateT1me0nS1zeChanged(object sender, SIzeChangedEventArgs args) { If (args.WldthChanged) { for (Int i = 0: i < 60; i++) xform[i].X = datetime.ActualWidth; MakeMask(): } } // Построение маски прозрачности void MakeMaskO { DrawlngGroup group = new DrawlngGroup(); Point ptCenter = new Po1nt(datet1me.Actualwidth + grd.Margin.Left, datetime.Actualwidth + grd.Margin.Left); // Построение 256 клиновидных делений, расположенных по кругу for (Int 1 = 0; 1 < 256; 1++) { Point ptlnnerl = new Pol nt(ptCenter.X + datetime.Actualwidth * Math.Cosd * 2 * Math.PI / 256), ptCenter.Y + datetime.Actualwidth * Math.S1n(1 * 2 * Math.PI / 256)); Point ptlnner2 = new Pol nt(ptCenter.X + datetime.Actual Width * Math.Cos((1 +2) *2* Math.PI / 256), ptCenter.Y + datetime.Actualwidth * Math.S1n((1 + 2) * 2 * Math.PI / 256));
940 Глава 31. Работа с графикой Point ptOuterl = new Pol nt(ptCenter.X + (datetime.ActualWidth + grd.Margin.Left) * Math.Cosd * 2 * Math.PI / 256), ptCenter.Y + (datetime.ActualWidth + grd.Margin.Left) * Math.Sind * 2 * Math.PI / 256)); Point ptOuter2 = new Point(ptCenter.X + (datetime.ActualWidth + grd.Margin.Left) * Math.Cos((i + 2) * 2 * Math.PI / 256). ptCenter.Y + (datetime.ActualWidth + grd.Margin.Left) * Math. Sin((i + 2) * 2 * Math.PI / 256)): PathSegmentCollection segcoll = new PathSegmentCollectionO: segcoll.Add(new LineSegment(ptInner2, false)); segcoll.Add(new LineSegment(ptOuter2, false)); segcoll.Add(new LineSegment(ptOuterl. false)); segcoll.Add(new LineSegment(ptInnerl. false)); PathFigure fig = new PathFigure(ptInnerl. segcoll. true); PathFigureCollection figcoll = new PathFigureCollectionO: figcoll.Add(fig): PathGeometry path = new PathGeometry(figcoll); byte byOpacity = (byte)Math.Min(255. 512 - 2 * i); SolidColorBrush br = new SolidColorBrush( Col or.FromArgb(byOpaci ty, clrBackground.R. clrBackground.G. clrBackground.B)); GeometryDrawing draw = new GeometryDrawing(br. new Pen(br. 2). path); group.Chi 1dren.Add(draw); } DrawingBrush brush = new DrawingBrush(group); mask.Fill = brush; } // Инициализация контекстного меню для строки даты/времени void ContextMenuOnOpened(object sender. RoutedEventArgs args) { ContextMenu menu = sender as ContextMenu; menu. Items.ClearO; stringEJ strFormats = { "d". "D". "f". "F", "g". "G". "M". "R". "s". "t". "T", "u". "LI". "Y" };
Работа с графикой 941 foreach (string strFormat in strFormats) { Menuitem item = new MenuItemO; item.Header = DateTime.Now.ToStrIng(strFormat): item.Tag = strFormat; Item.IsChecked = strFormat == (Resources["clock"] as ClockTIcker).Format: item.Click += MenuItemOnClick; menu.Items.Add(item); } // Обработка команды, выбранной в контекстном меню void MenuItemOnCllek(object sender, RoutedEventArgs args) { Menuitem item = (sender as Menuitem); (Resources["clock"] as ClockTicker).Format = item.Tag as string; } } } Пожалуй, самой сложной частью кода является метод MakeMask. Он отвечает за создание объекта DrawingBrush, используемого для задания свойства Fill эле- мента Ellipse; в XAML-файле он обозначается именем mask. Объект DrawingBrush в основном прозрачен, за исключением клиновидных областей по контуру кру- га. Эти клинья совпадают с делениями, отображаемыми на циферблате, и окра- шиваются в цвет фона циферблата. Вследствие применения маски видна только половина делений за эллипсом, от полной прозрачности до полной непрозрач- ности. При повороте эллипса кажется, что деления появляются на циферблате, а по- том постепенно исчезают. Альтернативное решение (возможно, более подходя- щее для меньшего количества деления) заключается в независимой анимации свойства Opacity для всех делений. Но мне больше нравится мой способ. Программа Hybridclock построена в основном из элементов, скрытых из вида. Невидимые элементы определяют структуру видимых элементов, а иногда, как при прохождении стрелки у делений 6:00 и 12:00), они меняются ролями.
Алфавитный указатель A—Z AddHandler, метод, 101, 177 Application, объект, 14, 17 ArcSegment, класс, 814 ArrangeOverride, метод, 212, 246, 843 Background, свойство, 32, 36, 63 Begin Animation, метод, 871 BindingNavigator, класс, 756 Bitmapimage, класс, 905 BitmapSource, класс, 905 Brush, класс, 36 Button, класс, 72 ButtonChrome, класс, 174 Canvas, класс, 140, 782 CheckBox, класс, 82 Click, событие, 72 Color, структура, 33 СОМ, 15 CoinbinedGeometry, класс, 806 ComboBox, класс, 339 Command, свойство, 79 Content, свойство, 54 ContentControl, класс, 54 ContentElement, класс, 177 Control, класс, 72 DataContext, свойство, 631, 744 DataTemplate, класс, 377 DataType, атрибут, 703 DateTime, класс, 557 Dependencyobject, класс, 157, 628 DependencyPropertyListView, класс, 379 Dispatcherobject, класс, 16 DispatchTimer, класс, 867 DockPanel, класс, 97, 114, 302 Drawlinage, класс, 914 DrawingContext, класс, 194, 811, 920 DrawingGroup, класс, 916 DrawingVisual, класс, 388 Ellipse, класс, 64 EventSetter, элемент XAML, 659 EventTrigger, класс, 872 FillRule, свойство, 788 FontSize, свойство, 55 Foreground, свойство, 63 Frame, элемент XAML, 588 FrameworkElement, класс, 60, 193 FrameworkTemplate, класс, 684 Geometry, класс, 802 GeometryGroup, класс, 806 GetVisualChild, метод, 211 Grid, класс, 118 GridSplitter, класс, 129 GridView, класс, 684 GroupBox, класс, 109 Header, свойство, 297 Height, свойство, 27 HorizontalAlignment, свойство, 66 lEnumerable, интерфейс, 267, 755 IList, интерфейс, 533, 755 Image, класс, 60, 904 элемент XAML, 504 IMultiValueConverter, интерфейс, 655 InitializeComponent, метод, 570 INotifyPropertyChanged, интерфейс, 740 Internet Explorer, 478 IsMouscOver, свойство, 228 Items, свойство, 267 ItemsControl, класс, 683,711 ItemsSource, свойство, 267 IValueConverter, интерфейс, 379, 638 LayoutTransform, свойство, 836 LinearGradientBrush, кисть, 41 класс, 527 ListBox, класс, 266, 764 элемент, 242 ListView, класс, 372, 684 Load, метод, 433 Main, метод, 17 Margin, свойство, 63 MatrixTransform, класс, 850 MeasureOverride, метод, 197, 211, 843 Menu, класс, 297 Menu Item, класс, 297 MessageBox, класс, 19 Microsoft Visual Studio, 15 MouseDown, событие, 17, 179 MouseUp, событие, 179 OnRender, метод, 194, 212, 811, 843 Panel, класс, 96 Path, класс, 802 PathGeometry, класс, 810 Pen, класс, 792 PointCollection, класс, 143 Polygon, класс, 142, 782 Polyline, класс, 782 объект, 107 PrcsentationHost.exe, программа, 478 PrintDialog, класс, 388
Алфавитный указатель 943 PrintTicket, класс, 388 RadialGradientBrush, класс, 48 RadioButton, класс, 82 RemoveHandler, метод, 177 RenderTargetBitmap, класс, 906 RenderTransform, свойство, 836 RepeatBehavior, атрибут, 876 ResizeMode, свойство, 32 ResourceDictionary, элемент XAML, 564 Resources, секция, 546, 659, 737 RichTextBox, класс, 92 RotateTransform, класс, 842 RoutedEventArgs, класс, 168 Run, метод, 15 Save, метод, 432 ScaleTransform, класс, 837 ScrollBar, класс, 135 Scroll Viewer, класс, 102 SeleclionChanged, событие, 270 Separator, класс, 298 Setter, элемент XAML, 659 Shape, класс, 781.904 Shapes, библиотека, 780 SolidColorBrush, класс, 36, 527 Sorted List, класс, 371 SourceName, свойство, 872 StackPanel, класс, 97 StartupUri, атрибут, 501 StaticRcsource, расширение, 651, 664 Stream Writer, объект, 481 Stroke, класс, 570 Style, элемент XAML, 659, 688 System, пространство имен, 14 System.Globalization, пространство имен, 648 System.IO, пространство имен, 355 System.Windows, пространство имен, 14, 41, 659 SystemPrinting, пространство имен, 388 TargetTypc, атрибут, 665 Template, свойство, 227 TextBlock, класс, 67 TextChanged, событие, 124 Text Range, класс, 95 this, ключевое слово, 26 TileBrush, класс, 922 Timeline, 871 Title, свойство, 17 ToggleButton, класс, 82 ToolBar, класс, 325 ToolTip, класс, 297 TreeView, класс, 352 TrecViewItem, класс, 354 UIEIement, класс, 17, 60, 166, 177 UniformGrid, класс, 244 Uri, класс, 483 ValueChanged, событие, 626 VerticalAlignment, свойство, 66 Visibility, свойство, 166 VisualChildrenCount, свойство, 211 Width, свойство, 27 Window, класс, 54 объект, 14 WPF (Windows Presentation Foundation), 14 WrapPanel, класс, 97 XAML Cruncher, программа, 505 XAML, язык, 475 ХВАР, формат, 565, 620 XML, язык, 475 XmlSerializer, класс, 431 XPath, свойство, 726 А—Я автономные XAML-файлы. 478 акселераторы, 313 альфа-канал, 33 анимация, 867 аппаратно-независимые единицы, 27 ассоциативный список, 315 атрибуты, 475 атрибуты свойств, 528 базы данных, 740 Безье, кривые, 811 Блокнот, 429 браузер, 620 векторная графика, 904 вешка разбивки, 118 визуальные объекты, 152 внешние отступы, 63, 99 гарнитура, 55 главное окно, 23 градиентные кисти, 42 группировка, 767 декораторы, 217 десериализация, 431 диалоговые окна, 388 дизайнер, 476 документы XAML, 477 завершители, 793 зависимые свойства, 154 замкнутые области, 788 заполнение, 788 захват мыши, 180 интерфейсные элементы, 60 источник, 626 итоговая продолжительность, 877 кадровые планы, 872 кисти, 904, 922 LinearGradientBrush, 41, 527 RadialGradientBrush, 48 SolidColorBrush, 36, 527 клавиатурный интерфейс, 296, 328, 411, 714 клиентская область, 33, 63, 184
944 Алфавитный указатель ключи ресурсов, 545 кнопки, 72 командная строка, 356 композитные изображения, 918 конвертор, 643 консольное окно, 16 контекстное меню, 320 контрольные точки, 817 кривые Безье, 811 локальное изолированное хранилище, 92 локальные ресурсы, 545 макетирование, 96 маршрутизация событий, 165 мастер, приложение, 597 меню, 297 метаданные, 155 метрический размер, 926 мини-язык, 825 многоугольники, 142 множественные привязки данных, 655 мозаичный режим, 927 навигация, 565, 754 наследование свойств, 153 ориентация страницы, 391 отложенная загрузка, 357 палитра, 241,910 панели, 96 панели инструментов, 325 панель задач, 23 парсер, 523 печать, 388 повторение анимации, 879 подменю, 297 представление данных, 755 преобразования, 832 привязка данных, 84, 470, 626, 744 привязка команд, 80 приемник, 626 присоединенные свойства, 115, 164, 247 Проводник Windows, 372 продолжительность анимации, 870 пространства имен System, 14 XAML, 477 пункты, 55 рабочая область, 29 растеризация, 828 растровая графика, 904 расширения разметки, 545, 553 режим растяжения, 926 ресурсы, 78, 522, 544 ресурсы сборок, 545 свойство содержимого, 536 семейства шрифтов, 55 сериализация, 430 системные цвета, 557 системы координат, 853 словарь, 315 события, общие сведения, 17 содержимое, 54 сортировка, 767 справочная система, 732 стиль, 659, 688 строка состояния, 350 субтраекторпи, 810 табуляция, 514 таймеры, 51 теги, 475 триггеры, 227, 687 указатель мыши, 179 файл программной логики, 487 фигуры, 810 фильтрация, 767 фокус ввода, 74, 166, 189 форматы пикселов, 905 формы ввода, 740 цвета, образцы, 268 цветовая таблица, 910 цветовое пространство RGB, 33 цикл сообщений, 15 шаблоны, 683 шрифты, 55 штрих, 570 экранные подсказки, 297 элементы свойств, 528 элементы управления, 72
Microsoft ® WINDOWS БАЗОВЫЙ КУРС PRESENTATION FOUNDATION В этой книге, написанной гуру Wi ndows-программ и рован ия Чарльзом Петцольдом, параллельно изложены два подхода программирования, на которых базируется Windows’Presentation Foundation (WPF). При чтении читатель будет цикличе- ски переходить от особенностей примене- ния XAML (Extensible Application Markup Language) к тонкостям программирования на С#, что позволит понять их взаимосвя- занность при программировании интерфей- сов. Уже с первой главы автор объясняет синтаксис XAML и принципы создания взаимосвязанного кода. Масса наглядных примеров помогут легко понять, как связы- ваются коды на XAML и С#. Все это плюс «фирменное» разъяснение Петцольда кон- цепций пользовательских интерфейсов (UI) легко научит Windows-разработчиков тому, как создавать для своих приложений интер- фейсы нового поколения. Издательский дом Питер Санкт-Петербург Б. Сампсониевский пр., 29а E-mail: sales@piter.com Internet: www.piter.com Тел./факс: (812) 703-7383 Издательство Русская Редакция Москва Шелепихинская наб., 32 E-mail: info@rusedit.com Internet: www.rusedit.com Тел./факс: (495) 256-7145 С^ППТЕР 203413 Его веб-сайт: Чарльз Петцольд уже двадцать лет пишет книги о программиро- вании для персональ- ных компьютеров. Его знаменитый труд «Programming Windows » вышел в пяти изданиях, оказал огромное влияние на целое поколение программистов и является одной из самых известных книг по программированию всех времен. Кроме того, его перу принадлежит книга «Code: The Hidden Language of Computer Hardware and Software» — повествование о внутренней жизни умных машин, получившее одобрение критиков. Чарльз является обладателем сертификата Microsoft MVP в области разработки клиентских приложений. htto.7/www. charlespetzold. com ISBN 978-5-91180-643-9 ISBN 978-5-7502-0341-3 Заказ книг: 197198, Санкт-Петербург, а/я 619 тел.: (812) 703-73-74, postbook@piter.com 61093, Харьков-93, а/я 9130 тел.: (057) 758-41-45, 751-10-02, piter@kharkov.piter.com Microsoft Windows Vista