Ан. Б. Шамшев КЛАССИЧЕСКИЕ ЭЛЕМЕНТЫ ПОЛЬЗОВАТЕЛЬСКОГО ИНТЕРФЕЙСА ВWINDOWSPRESENTATION FOUNDATION Ульяновск 2012 МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ федеральное государственное бюджетное образовательное учреждение высшего профессионального образования «УЛЬЯНОВСКИЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ» Ан. Б. Шамшев КЛАССИЧЕСКИЕ ЭЛЕМЕНТЫ ПОЛЬЗОВАТЕЛЬСКОГО ИНТЕРФЕЙСА В WINDOWSPRESENTATIONFOUN DATION Учебное пособие Ульяновск УлГТУ 2012 УДК 004.451(075) ББК 32.973.2-018.2я 7 Ш 19 Рецензенты: Кафедра «Информационные технологии» Ульяновского государственного университета (зав. кафедрой кандидат физико– математических наук, доцент М. А. Волков); Профессор кафедры «Информационные технологии»УлГУ, д-р техн.наук, И. В. Семушин. Утверждено редакционно-издательским советом университета в качестве учебного пособия Ш19 Шамшев, Ан. Б. Классические элементы пользовательского интерфейса в Windows Presentation Foundation : учебное пособие/ Ан. Б. Шамшев. – Ульяновск: УлГТУ, 2012. – 130 с. ISBN 978-5-9795-0925-9 Представлены базовые технологии проектирования интерфейсов с использованием технологии Windows Presentation Foundation для платформы MicrosoftdotNET. Особенности проектирования интерфейсов иллюстрируются примерами кодов разметки. Пособие предназначено для студентов направлений 230700.62 «Прикладная информатика» профиль «Прикладная информатика в экономике» и 231000.62 «Программная инженерия», изучающих дисциплину «Проектирование человеко-машинного интерфейса», а также для студентов других направлений, изучающих дисциплины, связанные с проектированием интерфейсов. УДК 004.451(075) ББК32.973.2-018.2я 7 ISBN 978-5-9795-0925-9 © Шамшев Ан. Б., 2012 © Оформление. УлГТУ, 2012 ПРЕДИСЛОВИЕ В учебном пособии представлены классические элементы пользовательского интерфейса и приведены примеры их использования в рамках процесса проектирования интерфейсов с использованием технологии Windows Presentation Foundation (WPF) для платформы Microsoft dotNET. Рассматриваемые примеры иллюстрируются примерами кодов разметки. Пособие предназначено для студентов направлений 230700.62 «Прикладная информатика» профиль «Прикладная информатика в экономике» и 231000.62 «Программная инженерия», изучающих дисциплину «Проектирование человеко-машинного интерфейса», а также разработчикам программных продуктов и пользовательских интерфейсов, желающим изучить современные технологии создания пользовательских интерфейсов. Выписка из ГОС ВПО: Цикл, к Компетенции студента, формируемые в результате освоения которому дисциплины относится дисциплина Б3.В.1.3 ПК-11: способность принимать участие в создании и управлении ИС на всех этапах жизненного цикла; В результате изучения дисциплины студент должен: знать основы построения пользовательских интерфейсов, основные способы и возможности среды разработки пользовательских интерфейсов, средства создания динамических интерфейсов и привязки интерфейсов к внешним данным; уметь формировать пользовательские интерфейсы при помощи средств разработки и языка описания интерфейсов XAML; владеть средствами разработки пользовательских интерфейсов Visual Studio 2010, технологией разработки интерфейсов Рассматриваются следующие разделы учебной программы: основы XAML, элементы управления и окна. 3 ВВЕДЕНИЕ В учебном пособии [4] была показана концепция формирования пользовательского интерфейса при помощи вложенных контейнеров компоновки. Подобная схема построения интерфейса обладает целым рядом достоинств, в частности, автоматическим масштабированием, адаптивностью и концепцией содержимого. Очевидно, что одной компоновки для создания пользовательского интерфейса недостаточно – ее необходимо наполнить элементами управления: текстом, изображениями, кнопками, переключателями и так далее. Для решения этой задачи используются, в частности, классические элементы управления, которыми Windows-разработчики пользуются уже много лет. Несмотря на то, что эти элементы управления используются уже довольно долго, технология Windows Presentation Foundation добавляет к ним несколько важных деталей. 1. КЛАСС CONTROL Окна WPF содержат элементы, однако только некоторые из них являются элементами управления. К ним относятся элементы, поддерживающие интерактивную связь с пользователем — они могут принимать фокус и получать входные данные от клавиатуры или мыши. Все элементы управления являются наследниками класса System.Windows.Control, который наделяет их базовыми характеристиками: они позволяют определять расположение содержимого внутри элемента управления; они позволяют определять порядок перехода с использованием клавиши табуляции; они поддерживают рисование фона, переднего плана и рамки: они поддерживают форматирование размера и шрифта текстового содержимого. 4 1.1. Кисти фона и переднего плана Практически любой элемент управления имеет фон и передний план. Как правило, фоном является поверхность элемента управления (например, белая или серая область внутри рамки кнопки), а передним планом — текст или иное содержимое. Цвет этих двух областей (но не содержимого) в WPF определяется с помощью свойств Background и Foreground соответственно. Следует отметить то, что свойства Background и Foreground не используют объекты цвета, в противоположность приложениям, созданным на основе технологии Windows Forms. В WPF эти свойства используют более универсальный объект — Brush (кисть). Благодаря этому можно осуществлять заливку содержимого фона и переднего плана сплошным цветом (с помощью кисти SolidColorBrush) или чем-либо более экзотическим (например, используя кисти LinearGradientBrush или TileBrush). В этом разделе будет рассмотрена только простая кисть SolidColorBrush, а ниже будут продемонстрированы другие ее варианты. 1.1.1. Установка цветов в коде Предположим, что необходимо установить поверхность голубого цвета внутри кнопки cmd. Ниже показан программный код, с помощью которого можно это сделать: cmd.Background = new SolidColorBrush(Colors.AliceBlue); Этот код создает новый объект SolidColorBrush с помощью готового цвета посредством статического свойства класса Colors. Имена свойств основаны на названиях цветов, которые поддерживаются большинством браузеров. Затем созданная кисть задается в качестве фоновой кисти кнопки, в результате чего фон кнопки становится светло-голубым. Также можно использовать системные цвета, которые учитывают предпочтения пользователя и цветовую схему, установленную в операционной системе. Эти цвета задаются перечислением System.Windows.SystemColors. Пример использования системных цветов (в данном случае в качестве цвета указан стандартный цвет фона кнопки): 5 cmd.Background = new SolidColorBrush(SystemColors.ControlColor); Поскольку системные кисти используются достаточно часто, класс SystemColors предлагает готовые свойства, возвращающие объект SolidColorBrush. Ниже показан пример их использования: cmd.Background = SystemColors.ControlBrush; Два приведенных примера являются эквивалентными и они оба обладают одним недостатком: если системный цвет будет изменен после того, как был запущен этот код, кнопка не будет обновлена, и новый цвет применяться не будет. Фактически, приведенный код делает мгновенный снимок текущего цвета или кисти. Для решения этой проблемы необходимо применять динамические ресурсы. Классы Colors и SystemColors позволяют просто и быстро задать цвет, однако существуют другие способы задания цвета. Например, можно создать объект Color, определяя значения R, G и В, каждое из которых соответствует красной, зеленой и синей составляющим цвета. Каждое из этих значений является числом из диапазона 0 - 255: int red = 0; int green = 255, int blue = 0; cmd.Foreground = new SolidColorBrush(Color.FromRgb(red, green, blue)); Также можно сделать цвет частично прозрачным, используя значение альфа-канала и вызывая метод Color.FromArgb(). Значение альфаканала, равное 255, соответствует полностью непрозрачному цвету, а значение 0 – полностью прозрачному. 1.1.2. RGB и scRGB Стандарт RGB применяется во многих программах. Например, можно получить RGB-значение цвета в программе для рисования и использовать этот же цвет в WPF-приложении. Однако возможна ситуация, в которой другие устройства (например, принтеры или плоттеры) могут поддерживать более широкий диапазон цветов. По этой причине был создан альтернативный стандарт scRGB, в котором каждый компонент цвета (альфаканал, красный, зеленый и синий) представлен с помощью 64-битных значений. 6 Структура цветов WPF поддерживает оба подхода. Она включает как набор стандартных свойств RGB (А, R, G и В), так и набор свойств scRGB (ScA, ScR, ScG и ScB). Эти свойства связаны между собой, поэтому если задать свойство R, то соответственным образом изменится и свойство ScR. Взаимосвязь между значениями RGB и значениями scRGB является нелинейной. Значение 0 в системе RGB соответствует значению 0 в scRGB, 255 в RGB соответствует 1 в scRGB, а все значения в диапазоне 0 – 255 в RGB представлены как десятичные значения в диапазоне 0 – 1 в scRGB. 1.2. Установка цветов в XAML Если задавать цвет фона или переднего плана средствами XAML, то можно воспользоваться сокращенным вариантом синтаксиса. Вместо определения объекта Brush достаточно задать наименование или значение цвета. Синтаксический анализатор WPF автоматически создаст объект SolidColorBrush c использованием выбранного цвета и будет применять этот объект для фона или переднего плана. Ниже показан пример, в котором используется имя цвета: <Button Background="Red">A Button</Button> Он эквивалентен следующему фрагменту разметки XAML: <Button>A Button <Button.Background> <SolidColorBrush Color="Red" /> </Button.Background> </Button> Если возникнет необходимость создать другой тип кисти (например, LinearGradientBrush), то нужно будет использовать полную форму синтаксиса и применять ее для рисования фона. Если необходим код цвета, то необходимо использовать менее удобный синтаксис, в котором значения R, G и В представляются в шестнадцатеричном формате. В WPF определены два формата: #rrggbb или #aarrggbb. Отличие между ними состоит в том, что последний формат содержит значение альфа-канала. Чтобы задать значения A, R, G и В, 7 понадобятся только две цифры, поскольку все они представляются в шестнадцатеричной форме. Ниже показан пример, который создает тот же цвет, что и в предыдущем фрагменте кода, с помощью записи #aarrggbb: <Button Background="#FFFF0000">A Button</Button> Здесь значением альфа-канала является FF (255), значением красной составляющей— FF (255), а значениями зеленой и синей — 0. Свойства Background и Foreground не единственные свойства, цвет которых можно определять при помощи кисти. Также можно нарисовать рамку вокруг элементов управления с помощью свойств BorderBrush и BorderThickness. Свойство BorderBrush принимает определенную разработчиком кисть, а свойство BorderThickness принимает ширину рамки в независящих от устройства единицах. Рамка будет отображена при установке обоих свойств. 1.3. Прозрачность В отличие от Windows Forms, в WPF поддерживается истинная прозрачность, если расположить несколько элементов один поверх другого и задать для каждого из них различную степень прозрачности, то можно увидеть в точности то, что и следует ожидать. Эта возможность дает возможность создать графический фон, который будет «просматриваться сквозь» элементы, расположенные на нем. При наличии необходимых навыков эта особенность позволит создавать многослойные анимационные объекты и другие эффекты, создание которых в других средах может оказаться чрезвычайно проблематичным. Сделать элемент прозрачным можно двумя способами: с помощью свойства Opacity. Свойство Opacity (непрозрачность) является дробным значением в диапазоне 0..1, где 1 соответствует полностью непрозрачному цвету, а 0 — полностью прозрачному. Свойство Opacity определяется в классе UIElement, поэтому оно может быть применено ко всем элементам; с помощью полупрозрачного цвета. Любой цвет, значение альфаканала которого составляет менее 255, является полупрозрачным. 8 Рекомендуется использовать прозрачные цвета вместо свойства Opacity. На рис. 1 показан пример, в котором имеется несколько полупрозрачных слоев. Рис. 1. Окно с полупрозрачными слоями Перечислим слои, формирующие рис. 1: окно имеет непрозрачный белый фон; панель верхнего уровня StackPanel, содержащая все элементы, имеет свойство ImageBrush, которое определяет изображение. Свойство Opacity этой кисти уменьшено для того, чтобы подсветить ее; это дает возможность видеть сквозь нее белый фон; в первой кнопке используется полупрозрачный красный цвет фона. Изображение просматривается сквозь фон кнопки, а текст является непрозрачным; метка (ниже первой кнопки) используется без заданной прозрачности; текстовое окно использует непрозрачный текст и непрозрачную рамку, а также полупрозрачный цвет фона; 9 еще одна панель StackPanel под текстовым окном использует TileBrush, чтобы создать шаблон с улыбающимся лицом. Кисть TileBrush имеет уменьшенное значение Opacity, поэтому фон просматривается сквозь нее; вторая панель StackPanel содержит элемент TextBlock с полностью прозрачным фоном и полупрозрачным белым текстом. Ниже показана разметка окна на рис. 1 в XAML. Следует отметить, что этот пример включает одну деталь, которая не была рассмотрена: специализированная кисть ImageBrush для рисования содержимого изображения. <StackPanel Margin="5"> <StackPanel.Background> <ImageBrush ImageSource="celestial.jpg" Opacity="0.7"/> </StackPanel.Background> <Button Foreground="White" FontSize="16" Margin="10" BorderBrush="White" Background="#60AA4030" Padding="20">A Semi-Transparent Button</Button> <Label Margin="10" FontSize="18" FontWeight="Bold" Foreground="White">Some Label Text</Label> <TextBox Margin="10" Background="#AAAAAAAA" Foreground="White" BorderBrush="White">A semi-transparent text box</TextBox> <Button Margin="10" Padding="25" BorderBrush="White" > <Button.Background> <ImageBrush ImageSource="happyface.jpg" Opacity="0.6" TileMode="Tile" Viewport="0,0,0.1,0.4"/> </Button.Background> <StackPanel> <TextBlock Foreground="#75FFFFFF" TextAlignment="Center" FontSize="30" FontWeight="Bold" TextWrapping="Wrap" >Semi-Transparent Layers</TextBlock> </StackPanel> </Button> </StackPanel> 10 Прозрачность является популярной возможностью WPF —она настолько проста и работает так хорошо, что является уже неотъемлемой частью пользовательского интерфейса WPF. 1.4. Шрифты Класс Control определяет небольшой набор свойств, связанных со шрифтами. Эти свойства определяют отображение текста в элементе управления и перечислены в таблице 1. Таблица 1 Свойства шрифтов, определенные в классе Control Имя Описание FontFamily Имя шрифта, который нужно использовать FontSize Размер шрифта в единицах, не зависящих от устройства FontStyle Наклонение текста, представленное объектом FontStyle. Необходимый предварительно заданный набор FontStyle получается из статических свойств класса FontStyles, который включает написание символов Normal, Italic или Oblique FontWeight Вес текста, представленный объектом FontWeight. Предварительно заданный набор FontWeight определяется статическими свойствами класса FontWeight. Полужирный (bold) является наиболее очевидным из них, хотя некоторые гарнитуры предлагают другие варианты, такие как Heavy, Light, ExtraBold и т. д. FontStretch Величина, на которую растягивается или сжимается текст, представленная объектом FontStretch. Необходимый предварительно заданный набор FontStretch определяется статическими свойствами класса FontStretches. Например, UltraCondensed сжимает текст до 50% от обычной ширины, a UltraExpanded расширяет их до 200% Очевидно, что наиболее важным из этих свойств является FontFamily. Семейство шрифтов (font family) представляет собой коллекцию связанных между собой гарнитур, например, Arial Regular, Arial Bold, Arial Italic и Arial Bold Italic являются частью семейства шрифтов Arial. Несмотря на то, что типографские правила и символы для каждой вариации 11 определяются отдельно, операционная система подразумевает, что все они связаны между собой. Поэтому можно конфигурировать элемент для использования Arial Regular, присвоить свойству FontWeight значение Bold и быть уверенными в том, что WPF будет использовать гарнитуру Arial Bold. При выборе шрифта необходимо указывать полное имя семейства, как показано ниже: <Button Name="cmd" FontFamily="Times New Roman" FontSize="18"> A Button</Button> Программный код практически аналогичен: cmd.FontFamily = "Times New Roman"; cmd.FontSize = "18"; При идентификации FontFamily нельзя использовать укороченную строку. Чтобы получить курсив или полужирный шрифт, можно (хотя и необязательно) использовать полное имя гарнитуры, как показано ниже: <Button FontFamily="Times New Roman Bold">A Button</Button> Тем не менее проще использовать просто имя семейства и задавать другие свойства (такие как FontStyle и FontWeight), чтобы получить требуемый вариант. Например, следующая разметка присваивает семейству шрифт Times New Roman, а весу шрифта — FontWeights.Bold: <Button FontFamily="Times New Roman" FontWeight="Bold">A Button</Button> 1.5. Текстовые декорации и типография Некоторые элементы поддерживают более сложную манипуляцию текстом при помощи свойств TextDecorations и Typography, которые позволяют украшать текст. Например, можно задать свойство TextDecorations с помощью статического свойства из класса TextDecorations. Оно предлагает только четыре декорации, каждая из которых позволяет добавить в текст некоторую разновидность линии. Они включают в себя Baseline, OverLine, Strikethrough и Underline. Свойство Typography является более сложным — оно позволяет получать доступ к специализированным вариантам гарнитур, которые могут предоставить лишь некоторые шрифты. В 12 качестве примера можно упомянуть различные выравнивания чисел, лигатуры (связи между соседними буквами) и капители. На практике особенности TextDecorations и Typography находят свое применение только в содержимом потоковых документов. Однако их можно применять и в классе TextBox. Кроме того, они поддерживаются элементом управления TextBlock, являющимся облегченной версией Label и прекрасно подходящим для показа небольших объемов текстового содержимого и допускающего перенос текста. Несмотря на то, что применение TextDecorations в элементе управления TextBox маловероятно или будет изменено его свойство Typography, в некоторых случаях может понадобиться использовать подчеркивание в TextBlock. Соответствующий пример показан ниже: <TextBlock TextDecorations="Underline">Underlined text</TextBlock> 1.5.1. Наследование шрифтов Когда задается одно из свойств шрифта, значение этого свойства проходит сквозь вложенные объекты. Например, если задать свойство FontFamily для окна верхнего уровня, то каждый элемент управления в данном окне получит это же значение FontFamily до элемента, который явно переопределит это свойство. Эта особенность работает благодаря тому, что свойства шрифтов являются свойствами зависимостей, а одной из возможностей, предоставляемых свойствами зависимостей, является наследование значений свойств — в данном случае процесс передачи параметров шрифта всем вложенным элементам управления. Следует отметить, что наследование значения свойства может осуществляться в элементах, которые не поддерживают это свойство. Например, пусть создано окно, в котором имеется панель StackPanel, а внутри нее три метки Label. Можно задать свойство FontSize окна, поскольку класс Window происходит от класса Control. Панели непосредственно задать свойство FontSize нельзя, так как она не является элементом управления. Тем не менее заданное свойство FontSize окна пройдет «сквозь» 13 панель StackPanel, и его получат метки, которые изменят размер своего шрифта. Наряду с параметрами шрифтов в некоторых других базовых свойствах используется наследование значений свойств. В частности, наследование применяется свойством Foreground в классе Control. Отметим также, что свойство Background не использует наследования. Тем не менее фон, заданный по умолчанию, представляет собой пустую ссылку, которая визуализируется большинством элементов управления в виде прозрачного фона. Это означает, что родительский фон будет просматриваться, как было показано на рис.1. В классе UIElement наследование поддерживается свойствами AllowDrop, IsEnabled и IsVisible. В классе FrameworkElement наследование поддерживается свойствами CultureInfo и FlowDirection. 1.5.2. Замена шрифтов При установке шрифтов необходимо основательно подходить к выбору шрифта и желательно выяснить заранее, будет ли он поддерживаться на клиентском компьютере. Однако WPF может помочь в этом вопросе благодаря системе обхода шрифтов. Для этого следует указать в свойстве FontFamily список шрифтов, разделенных запятыми. После этого WPF выберет определенный на клиентском компьютере шрифт из заданного списка. Ниже показан пример, в котором производится попытка использовать шрифт Technical Italic, а в случае невозможности его использования будут выбран шрифт Comic Sans MS или Arial, если отсутствуют оба вышеуказанных: <Button FontFamily="Technical Italic, Comic Sans MS, Arial">A Button</Button> Следует обратить внимание на следующее правило записи: если семейство шрифтов действительно будет содержать запятую в своем имени, нужно будет написать ее в строке дважды. Можно получить список шрифтов, установленных на текущем компьютере, с помощью статической коллекции System.FontFamilies класса 14 System.Windows.Media.Fonts. Ниже показан пример, в котором эта коллекция используется для добавления шрифтов в окно списка: foreach (FontFamily fontFamily in Fonts.SystemFontFamilies) { lstFonts.Items.Add(fontFamily.Source); } Объект FontFamily позволяет проверить другие детали, такие как междустрочный интервал и связанные гарнитуры. 1.5.3. Встраивание шрифтов Альтернативным вариантом при работе с необычными шрифтами является их встраивание в приложение. Благодаря такой возможности, разработанное приложение никогда не будет иметь проблем с нахождением требуемого шрифта. Процесс встраивания достаточно прост. Сначала файл шрифта (чаще всего файл с расширением .ttf) добавляется в приложение, и параметру Build Action присваивается значение Resource. Это действие выполняется в Visual Studio. Для этого нужно выбрать файл шрифта в Solution Explorer и изменить Build Action в окне Properties (Свойства) (рис. 2): Рис. 2. Встраивание шрифта Затем, при использовании шрифта, необходимо добавить символьную последовательность ./# перед именем семейства, как показано ниже: <Label Name="tst" FontSize="20" FontFamily="./#Bayern" >This is an embedded font</Label> 15 После символьной последовательности ./# можно указать имя файла, однако обычно добавляется просто знак числа (#) и имя семейства шрифтов. В показанном выше примере встроенный шрифт получил имя Вауеrn. Окно с надписью, выполненной встроенным шрифтом, показано на рис. 3: Рис. 3. Пример встраивания шрифта 1.5.4. Указатели мыши В любом качественно разработанном приложении нужно делать так, чтобы указатель мыши показывал, что приложение занято, или отражал работу разных элементов управления. Можно задать указатель мыши для любого элемента, используя свойство Cursor, которое является наследником класса FrameworkElement. Каждый указатель представляется объектом System.Windows.Input. Cursor. Самый простой способ получить объект Cursor – использовать статические свойства класса Cursors из пространства имен System.Windows.Input. Они включают все стандартные указатели Windows, такие как песочные часы, рука, стрелки изменения размеров и т. д. Ниже показан пример, в котором для текущего окна определяется курсор «песочные часы»: this.Cursor = Cursors.Wait; Теперь при перемещении указателя мыши в текущем окне указатель примет вид песочных часов (Windows XP) или водоворота (Windows Vista, Windows 7). Следует отметить, что свойства класса Cursors используют указатели, определенные в операционной системе. Если пользователь настроит набор стандартных указателей, созданное приложение будет использовать специальные указатели. Если задавать указатель средствами XAML, то нет необходимости использовать класс Cursors напрямую, поскольку конвертер типов 16 TypeConverter для свойства Cursor может распознавать имена свойств и получать соответствующий объект Cursor из класса Cursors. Таким образом, можно написать разметку, подобную приведенной ниже, чтобы отобразить курсор «справки» (комбинация стрелки и вопросительного знака), когда указатель мыши будет наведен на кнопку: <Button Cursor="Help">Help</Button> Параметры указателя можно менять. Например, можно задать разные указатели для кнопки и окна, в которой она находится. Указатель кнопки будет отображаться при наведении на кнопку, а указатель окна будет использоваться в любом другом участке окна. Единственное исключение из данного правила состоит в том, что родитель может переопределить параметры указателя своих потомков с помощью свойства ForceCursor. Если этому свойству будет присвоено значение true, свойство потомка Cursor будет проигнорировано, в то время как родительское свойство Cursor будет применено повсеместно. Если необходимо применить параметры указателя к каждому элементу в каждом окне приложения, то необходимо использовать статическое свойство Mouse.OverrideCursor, которое переопределяет свойство Cursor каждого элемента: Mouse.OverrideCursor = Cursors.Wait; Чтобы отменить это переопределение, действующее в рамках всего приложения, достаточно присвоить свойству Mouse.OverrideCursor значение null. Также WPF поддерживает использование специальных указателей. Можно применять как обычные файлы указателей .cur, так и файлы анимированных указателей .ani. Чтобы использовать специальный указатель, нужно передать имя файла указателя или поток вместе с данными указателя конструктору объекта Cursor: Cursor customCursor = new Cursor (System.IO.Path.Combine(applicationDir, "stopwatch.ani")); this.Cursor = customCursor; 17 Объект Cursor не поддерживает напрямую синтаксис URI, который позволяет другим элементам WPF работать с файлами, хранящимися в скомпилированной сборке. Однако можно добавить файл указателя в приложение в качестве ресурса, а затем извлечь его как поток, который можно будет использовать для создания объекта Cursor. Для этой цели предназначен метод Application.GetResourceStream(): StreamResourcelnfo sri = Application.GetResourceStream(new Uri("stopwatch.ani", UriKind.Relative)); Cursor customCursor = new Cursor(sri.Stream); this.Cursor = customCursor; Этот код подразумевает, что в проект был добавлен файл stopwatch.ani и параметру Build Action было присвоено значение Resource. 2. ЭЛЕМЕНТЫ УПРАВЛЕНИЯ СОДЕРЖИМЫМ Как было сказано в [4], многие основные элементы управления WPF связаны с управлением содержимым. К их числу относятся такие элементы управления, как Label, Button, CheckBox и RadioButton. 2.1. Метки Простейшим элементом управления содержимым является Label — метка. Как и любой другой элемент управления содержимым, она принимает одиночную порцию содержимого, которую нужно поместить внутри нее. Отличительной чертой элемента Label является поддержка мнемонических команд — клавиш быстрого доступа, которые передают фокус связанному элементу управления. Для обеспечения поддержки этой функции элемент управления Label предлагает свойство Target. Для задания этого свойства необходимо воспользоваться выражением привязки, которое будет указывать на другой элемент управления. Ниже показан синтаксис, который нужно использовать для этой цели: <Label Target="{Binding ElementName=txtA}">Choose _A</Label> <TextBox Name="txtA"></TextBox> 18 <Label Target="{Binding ElementName=txtB}">Choose _B</Label> <TextBox Name="txtB"></TextBox> Символ подчеркивания в тексте метки указывает на клавишу быстрого доступа. Если в метке нужно отобразить символ подчеркивания, нужно добавить два таких символа. Все мнемонические команды работают при одновременном нажатии клавиши <Alt> и заданной клавиши быстрого доступа. Если в данном примере пользователь нажмет комбинацию <Alt+A>, то первая метка передаст фокус связанному элементу управления, которым в данном случае является txtA. Точно так же нажатие комбинации <A1t+B> приводит к передаче фокуса элементу управления txtB. Обычно буквы клавиш быстрого доступа скрыты до тех пор, пока пользователь не нажмет <Alt>, после чего они отмечаются подчеркиванием (рис. 4). Такое поведение метки определяется параметрами системы. Рис. 4. Использование меток и клавиш быстрого доступа Если необходимо только отображать содержимое без поддержки мнемонических команд, то рекомендуется применять более облегченный элемент TextBlock. В отличие от элемента управления Label, TextBlock поддерживает перенос текста с помощью свойства TextWrapping. 19 2.2. Кнопки WPF распознает три типа кнопок: Button, CheckBox и RadioButton. Все эти кнопки представляют собой элементы управления содержимым, являющимися наследниками класса ButtonBase. Класс ButtonBase включает несколько членов. Он определяет событие Click и добавляет поддержку команд, которые позволят подключить кнопки для высокоуровневых задач. Также класс ButtonBase добавляет свойство ClickMode, которое определяет, когда кнопка генерирует событие Click в ответ на действия мыши. Значением, используемым по умолчанию, является ClickMode.Release. Это значение определяет, что событие Click будет сгенерировано при нажатии и отпускании кнопки мыши. Однако можно таким образом определить поведение кнопки, чтобы событие Click возникало при первом нажатии кнопки мыши (ClickMode.Press) или всякий раз, когда указатель мыши будет наведен на кнопку и задержан над ней (ClickMode.Hover). Все кнопки поддерживают клавиши доступа, которые работают подобно мнемоническим командам в элементе управления Label. Чтобы обозначить клавишу доступа, нужно добавить символ подчеркивания. Когда пользователь нажмет клавишу <Alt> и клавишу доступа, возникнет событие Click кнопки. 2.2.1. Класс Button Класс Button представляет стандартную кнопку Windows. Он добавляет всего два свойства, доступные для записи: IsCancel и IsDefault. Если свойство IsCancel имеет значение true, то эта кнопка будет работать как кнопка отмены окна. Если нажать клавишу <Esc>, когда текущее окно будет находиться в фокусе, то кнопка будет приведена в действие. Если свойство IsDefault имеет значение true, то эта кнопка считается кнопкой, используемой по умолчанию или кнопкой принятия. Ее поведение зависит от того, где находится в данный момент указатель мыши в окне. Если указатель мыши наведен на элемент управления, отличный от Button (например, TextBox, RadioButton, CheckBox и т. д.), то кнопка, используемая по умолчанию, будет затенена голубым цветом — почти так, как если 20 бы она находилась в фокусе. Если нажать клавишу <Enter>, кнопка будет приведена в действие. Однако если указатель мыши наведен на другой элемент управления Button, то текущая кнопка будет затенена голубым цветом, и при нажатии <Enter> будет приведена в действие именно эта кнопка, а не кнопка по умолчанию. Многие пользователи используют эти клавиши быстрого доступа, поэтому разумно определить эти детали в каждом создаваемом окне. Для кнопки по умолчанию и кнопки отмены следует написать код обработки события, так как WPF не поддерживает это поведение. В некоторых случаях имеет смысл сделать так, чтобы одна и та же кнопка в окне являлась и кнопкой отмены, и кнопкой по умолчанию. В качестве такого примера можно указать кнопку OK в окне или программе. При назначении кнопки отмены и кнопки по умолчанию следует помнить, что в окне должна быть только одна кнопка отмены и одна кнопка по умолчанию. Если назначить несколько кнопок отмены, то при нажатии клавиши <Esc> будет просто передаваться фокус следующей кнопке по умолчанию без ее активизации. Если определено несколько кнопок по умолчанию, нажатие клавиши <Enter> приведет к неочевидному поведению. Если в фокусе будет находиться элемент управления, отличный от Button, то при нажатии <Enter> фокус будет передан следующей кнопке по умолчанию. Если же в фокусе находится элемент управления Button, нажатие клавиши <Enter> активизирует ее. Класс Button включает также свойство IsDefaulted, которое доступно только для чтения. IsDefaulted возвращает значение true для кнопки по умолчанию, если в фокусе находится другой элемент управления, не принимающий клавишу <Enter>. В этой ситуации нажатие <Enter> приведет к активизации кнопки. Например, элемент управления TextBox не принимает клавишу <Enter>, если не присвоить свойству TextBox.AcceptsReturn значение true. Если элемент управления TextBox, свойство TextBox.AcceptsReturn которого имеет значение true, находится в фокусе, то свойство IsDefaulted кнопки по умолчанию будет иметь значение false. Если элемент управления TextBox, свойство AcceptsReturn которого имеет 21 значение false, получает фокус, то свойство IsDefaulted кнопки по умолчанию получает значение true. Свойство IsDefaulted возвращает значение false, когда кнопка находится в фокусе, даже если нажатие клавиши <Enter> в этом месте приводит к активизации кнопки. Это свойство чаще всего используется для написания триггеров стилей. 2.2.3. Классы ToggleButton и RepeatButton Помимо Button, еще три класса являются потомками класса ButtonBase: GridViewColumnHeader, который представляет заголовок столбца, активизируемый щелчком кнопкой мыши, если используется сеточный элемент ListView; RepeatButton, который будет непрерывно генерировать события Click, если пользователь нажмет и будет удерживать нажатой кнопку. Обычные кнопки генерируют событие Click при однократном нажатии кнопки; ToggleButton, который представляет кнопку, имеющую два состояния _ нажата и отпущена. Если щелкнуть на кнопке ToggleButton, она будет оставаться нажатой до тех пор, пока пользователь не щелкнет на ней снова. Классы RepeatButton и ToggleButton определены в пространстве имен System.Windows.Controls.Primitives. Чаще всего они используются для построения более сложных элементов управления, создавая или расширяя возможности путем наследования. Например, RepeatButton используется для создания высокоуровневого элемента управления ScrollBar, который является частью еще более высокоуровневого элемента ScrollViewer, RepeatButton придает кнопкам со стрелками в конце линейки прокрутки их особое поведение — прокрутка продолжается до тех пор, пока пользователь их нажимает. ToggleButton применяется для порождения классов CheckBox и RadioButton, которые будут рассмотрены далее. 22 2.2.4. Элемент управления CheckBox Кнопки CheckBox и RadioButton — это кнопки другого вида. Они являются потомками класса ToggleButton. Они предоставляют пользователю возможность включать и выключать их. В случае CheckBox включение элемента управления означает отметку в нем флажка. Класс CheckBox не добавляет никаких членов, поэтому базовый интерфейс CheckBox определяется в классе ToggleButton. Кроме того, ToggleButton добавляет свойство IsChecked. Свойство IsChecked может принимать обнуляемое булевское значение, т. е. оно может принимать значения true, false или null. Очевидно, что true представляет отмеченный флажок, a false— пустое место. Значение null используется для представления промежуточного состояния, которое отображается в виде затененного окошка. Это состояние обычно служит для того, чтобы представить значения, которые являются неопределенными по какой-либо причине. Например, если имеется флажок, который позволяет применять полужирный шрифт в текстовом приложении, а текущий выбор включает как полужирный, так и обычный текст, можно присвоить флажку значение null, чтобы отображать промежуточное состояние. Чтобы присвоить значение null в разметке WPF, нужно использовать расширение разметки Null, как показано ниже: <CheckBox IsChecked="{x:Null}">А check box in indeterminate state </CheckBox> Наряду со свойством IsChecked класс ToggleButton добавляет свойство IsThreeState, которое определяет, может ли пользователь вводить флажок в промежуточное состояние. Если свойство IsThreeState будет иметь значение false (значение по умолчанию), то при щелчке флажок будет менять свое состояние между «отмечен» и «не отмечен», а промежуточное состояние можно задать только с помощью программного кода. В противном случае щелчки на флажке будут по очереди давать три возможных состояния. Класс ToggleButton определяет также три события, возникающие, когда флажок принимает одно из определенных состояний: Checked, Un23 checked и Intermediate. В большинстве случаев проще всего внедрить эту логику в один из обработчиков событий, обрабатывая событие Click, наследуемое от класса ButtonBase. Событие Click возникает всякий раз, когда кнопка меняет свое состояние. 2.2.5. Элемент управления RadioButton RadioButton также является наследником класса ToggleButton и использует то же свойство IsChecked и те же события Checked, Unchecked и Intermediate. Также RadioButton добавляет свойство GroupName, которое позволяет управлять расположением переключателей в группах. Обычно группировка переключателей RadioButton определяется их контейнером: если поместить три элемента управления RadioButton в панели StackPanel, то они сформируют группу, в которой пользователь сможет выбрать только один из них. Следовательно, если поместить комбинацию переключателей в две разных панели StackPanel, то образуются две независимые группы. Свойство GroupName позволяет переопределить это поведение. Его можно использовать для того, чтобы создать несколько групп в одном и том же контейнере, или чтобы создать одну группу, которая будет охватывать несколько контейнеров. В обоих случаях достаточно просто присвоить всем переключателям, принадлежащим друг другу, имя одной и той же группы. Рассмотрим следующий пример: <StackPanel> <GroupBox Margin="5"> <StackPanel> <RadioButton>Group 1</RadioButton> <RadioButton>Group 1</RadioButton> <RadioButton>Group 1</RadioButton> <RadioButton Margin="0,10,0,0" GroupName="Group2">Group 2 </RadioButton> </StackPanel> </GroupBox> <GroupBox Margin="5"> 24 <StackPanel> <RadioButton>Group 3</RadioButton> <RadioButton>Group 3</RadioButton> <RadioButton>Group 3</RadioButton> <RadioButton Margin="0,10,0,0" GroupName="Group2">Group 2 </RadioButton> </StackPanel> </GroupBox> </StackPanel> В данном примере определены два контейнера, вмещающих переключатели, и три группы. Последний переключатель внизу каждого группового окна является частью третьей группы. Такая компоновка была придумана специально для демонстрации подобной возможности, однако в реальности могут существовать задачи, в которых нужно отделять определенный переключатель таким образом, чтобы он не утратил членства в группе. Внешний вид окна, определяемого данной разметкой, приведен на рис. 5. Рис. 5. Группировка элементов RadioButton 2.3. Контекстные окна указателя WPF предлагает гибкую модель работы с контекстными окнами указателя или tooltips — желтыми прямоугольниками, которые появляются, когда пользователь наводит указатель мыши на некоторый объект, его интересующий. Поскольку контекстные окна указателя в WPF относятся к группе элементов управления содержимым, в контекстное окно можно 25 поместить практически все, что необходимо. Также можно настроить различные временные параметры, чтобы задать частоту появления и исчезновения контекстных окон указателя. Простейший способ показать контекстное окно указателя состоит в определении свойства ToolTip элемента, а не использовании класса ToolTip напрямую. Свойство ToolTip определено в классе FramworkElement, поэтому указатель будет доступен везде, где он будет размещен в окне WPF. Например, ниже показана кнопка с базовым контекстным окном указателя: <Button ToolTip="This is my tooltip">I have a tooltip</Button> Когда пользователь наведет на нее указатель мыши, то в стандартном окошке появится текст: This is my tooltip". Вид окна с контекстным окном указателя представлен на рис. 6. Рис. 6. Пример базового контекстного указателя Если необходимо создать более сложное содержимое контекстного окна указателя (например, комбинацию вложенных элементов), то следует разбить свойство ToolTip на отдельные элементы. Ниже показан пример, в котором задается свойство ToolTip с помощью сложного вложенного содержимого: <Button ToolTipService.InitialShowDelay="0"> <Button.ToolTip> <ToolTip Background="#60AA4030" Foreground="White" HasDropShadow="False" > 26 <StackPanel> <TextBlock Margin="3" >Image and text</TextBlock> <Image Source="happyface.jpg" Stretch="None" /> <TextBlock Margin="3" >Image and text</TextBlock> </StackPanel> </ToolTip> </Button.ToolTip> <Button.Content>I have a fancy tooltip</Button.Content> </Button> Как и в предыдущем примере, WPF неявно создает объект ToolTip. Разница состоит в том, что в данном случае объект ToolTip содержит панель StackPanel, а не простую строку. На рис. 7 показано соответствующее окно. Рис. 7. Пример составного контекстного указателя Если несколько окон указателя будут перекрывать друг друга, то будет отображено специальное контекстное окно указателя. Например, если добавить контекстное окно указателя в контейнер StackPanel рассматриваемого примера, то это окно появится, когда пользователь наведет указатель мыши на пустое место панели или элемент управления, не имеющий собственного контекстного окна указателя. 27 2.4. Настройка параметров контекстного окна указателя В показанном выше примере было показано, как можно настроить содержимое контекстного окна указателя. Теперь рассмотрим настройку других параметров, связанных с работой контекстного окна указателя. В этом случае существуют два варианта. Первым из них является явное определение объект ToolTip. Это дает возможность напрямую задать разнообразные свойства ToolTip. ToolTip — это элемент управления содержимым, поэтому можно настроить стандартные свойства, такие как Background (чтобы сменить стандартный фоновый цвет), Padding и Font. Также можно изменить свойства, определенные в классе ToolTip (они перечислены в таблице 2). Большинство из этих свойств предназначено для того, чтобы помочь поместить контекстное окно указателя в необходимое место на экране. Таблица 2 Свойства контекстного окна указателя Имя HasDropShadow Описание Определяет, имеет ли контекстное окно указателя расплывчатую черную тень, которая выделяет его на фоне расположенного за ним окна Placement Определяет, как будет позиционировано контекстное окно указателя, используя одно из значений из перечисления PlacementMode. Значением по умолчанию является Mouse, которое означает, что верхний левый угол контекстного окна указателя будет располагаться относительно текущей позиции указателя мыши. Кроме того, можно задавать месторасположение контекстного окна указателя с помощью абсолютных координат экрана или размещать его относительно некоторого элемента HorizontalOffset VerticalOffset и Позволяют задать контекстному окну указателя точное месторасположение. Можно использовать как положительные, так и отрицательные значения PlacementTarget Позволяет поместить контекстное окно указателя относительно другого элемента. Чтобы использовать это свойство, 28 Окончание таблицы 2 Имя PlacementTarget Описание свойство Placement должно иметь одно из следующих значений: Left, Right, Тор или Bottom. Свойство определяет крайнюю точку элемента, по отношению к которому будет производиться выравнивание контекстного окна указателя PlacementRectangle Позволяет поместить контекстное окно указателя со смещением. Работает точно так же, как и свойства HorizontalOffset и VerticalOffset. Это свойство не работает, если свойство Placement имеет иметь значение Mouse CustomPopupPlace- Позволяет определять местоположение контекстного окна tomPopupPlace- указателя динамически с помощью кода mentCallback С помощью свойств ToolTip следующая разметка создает контекстное окно указателя, которое не имеет тени, но использует прозрачный красный фон, который позволяет видеть находящееся за ним окно (и элементы управления, имеющиеся в нем): <Button> <Button.ToolTip> <ToolTip Background="#60AA4030" Foreground="White" HasDropShadow="False" > <StackPanel> <TextBlock Margin="3" >Image and text</TextBlock> <Image Source="happyface.jpg" Stretch="None" /> <TextBlock Margin="3" >Image and text</TextBlock> </StackPanel> </ToolTip> </Button.ToolTip> <Button.Content>I have a fancy tooltip</Button.Content> </Button> В большинстве случаев достаточно использовать стандартное размещение контекстного окна указателя — в текущей позиции указателя мыши. Однако разнообразные свойства ToolTip предлагают множество других вариантов размещения. Ниже перечислены стратегии, согласно которым можно определить местоположение контекстного окна указателя: 29 привязка к текущей позиции указателя мыши. Это стандартный способ, который основан на том, что свойству Placement присваивается значение Mouse. Левый верхний угол контекстного окна указателя присоединяется к левому верхнему углу невидимого «ограничивающего прямоугольника» указателя мыши; привязка к позиции элемента, на который наведен указатель мыши. Свойству Placement присваивается значение Left, Right, Top, Bottom или Center, в зависимости от того, где находится край элемента, который используется для привязки. Левый верхний угол контекстного окна указателя будет присоединен к этому краю; привязка к позиции другого элемента (или окна). Свойство Placement задается точно так же, как при присоединении контекстного окна указателя к текущему элементу. Затем выбирается элемент путем задания свойства PlacementTarget; определение смещения. Используется одна из вышеперечисленных стратегий, а также определяются свойства HorizontalOffset и VerticalOffset, с помощью которых можно получить небольшое дополнительное пространство; использование абсолютных координат. Свойству Placement присваивается значение Absolute, а с помощью свойств HorizontalOffset и VerticalOffset (или PlacementRectangle) задается пространство между контекстным окном указателя и левым верхним углом окна; осуществление расчетов во время выполнения. Свойству Placement присваивается значение Custom. С помощью свойства CustomPopupPlacementCallback определяется метод, выполняющий расчет координат. 2.4.1. Настройка свойств ToolTipService Существуют некоторые свойства контекстного окна указателя, которые нельзя сконфигурировать с помощью свойств класса ToolTip. Для 30 этой цели следует использовать класс ToolTipService. Он позволяет конфигурировать временные задержки, связанные с отображением контекстного окна указателя. Все свойства этого класса являются прикрепленными, поэтому можно задавать их прямо в дескрипторе элемента управления, как показано ниже: <Button ToolTipService.InitialShowDelay="1"> </Button> Класс ToolTipService определяет много тех же свойств, что и класс ToolTip. Поэтому при работе с контекстными окнами указателя, содержащими только текст, можно применять очень простой синтаксис. Вместо того чтобы добавлять вложенный элемент ToolTip, можно задать все необходимое с помощью атрибутов: <Button ToolTip="This tooltip is aligned with the bottom edge" ToolTipService.Placement="Bottom">I have a tooltip</Button> Свойства класса ToolTipService перечислены в таблице 3. Таблица 3 Свойства класса ToolTipService Имя InitialShowDelay Описание Задает временную задержку (в миллисекундах), по истечении которой контекстное окно указателя будет отображено, если указатель будет наведен на элемент ShowDuration Задает промежуток времени (в миллисекундах), в течение которого будет отображаться контекстное окно указателя, а затем исчезнет с экрана, если пользователь не будет перемещать указатель мыши BetweenShowDelay BetweenShowDelay Задает временное окно (в миллисекундах), в течение которого пользователь может переходить от одного контекстного окна указателя к другому без задержки, определяемой свойством InitialShowDelay ToolTip Задает содержимое контекстного окна указателя. Установка свойства ToolTipService.ToolTip эквивалентно заданию свойства FrameworkElement.ToolTip элемента HasDropShadow Определяет, будет ли контекстное окно указателя иметь тень, выделяющую его на фоне находящегося за ним окна ShowOnDisabled Определяет поведение контекстного окна указателя, если 31 Окончание таблицы 3 Имя ShowOnDisabled Описание связанный с ним элемент отключен. Если это свойство имеет значение true, контекстное окно указателя будет отображаться для отключенных элементов. По умолчанию этому свойству присваивается значение false Placement, Позволяют управлять местоположением контекстного окна PlacementTarget, указателя. Эти свойства работают точно так же, как и анало- PlacementRectangle, гичные им свойства класса ToolTip HorizontalOffset VerticalOffset IsEnabled и IsOpen Позволяют управлять контекстным окном указателя в коде. IsEnabled дает возможность временно отключить ToolTip, а IsOpen позволяет программным образом показать или скрыть контекстное окно указателя или проверить, открыто ли оно В этом классе также определены два маршрутизируемых события: ToolTipOpening и ToolTipClosing. Можно реагировать на эти события, чтобы заполнить контекстное окно указателя оперативным содержимым, или же для того, чтобы переопределить способ работы контекстного окна указателя. 2.5. Элемент управления Popup Элемент управления Popup имеет много общего с элементом ToolTip, хотя они не являются наследниками друг друга. Как и ToolTip, элемент Popup может включать порцию содержимого, которое может нести в себе любой элемент WPF. Содержимое элемента хранится в свойстве Popup.Child, а не в свойстве ToolTip.Content. Как и в элементе управления ToolTip, содержимое Popup может распространяться за пределы окна. Параметры местоположения элемента Popup можно задать с помощью тех же свойств, а показать и скрыть его можно с помощью того же свойства IsOpen. Различия между элементами Popup и ToolTip являются очень важными и заключаются в следующем: 32 Popup никогда не отображается автоматически. Чтобы этот элемент управления отобразился на экране, необходимо позаботиться об этом заранее; свойство Popup.StaysOpen по умолчанию имеет значение true, поэтому элемент управления Popup не исчезнет с экрана до тех пор, пока явным образом не будет присвоено свойству Popup.StaysOpen значение false. Если присвоить свойству StaysOpen значение false, элемент управления Popup исчезнет с экрана, как только пользователь щелкнет где-нибудь на экране; элемент управления Popup имеет свойство PopupAnimation, которое позволяет управлять отображением упомянутого элемента управления, когда его свойство IsOpen имеет значение true. Данное свойство может принимать следующие значения: o None – значение по умолчанию; o Fade – постепенное увеличение непрозрачности всплывающего окна; o Scroll – непрозрачность плавно переходит с левого верхнего угла окна, пока позволяет пространство; o Slide – окно скользит на свое место, пока позволяет пространство. Чтобы любой из этих анимационных эффектов мог работать, необходимо присвоить свойству AllowsTransparency значение true; элемент управления Popup может принимать фокус. Таким образом, можно помещать в него элементы управления, поддерживающие интерактивную связь с пользователем (например, Button). Эта возможность является одной из ключевых причин использования элемента Popup вместо ToolTip; элемент управления Popup определен в пространстве имен System.Windows.Controls.Primitives, так как он чаще всего используется в качестве строительного блока для более сложных элементов 33 управления. Следует отметить, что Popup не является столь простым в использовании, как другие элементы управления. Например, необходимо задавать свойство Background, если нужно видеть содержимое, поскольку оно не наследуется от окна, и разработчику придется добавлять рамку (например, при помощи элемента Border). Поскольку элемент управления Popup нужно отображать вручную, его можно полностью создавать в программном коде или в разметке XAML — достаточно лишь определить свойство Name, чтобы впоследствии им можно было манипулировать. На рис. 8 показан пример использования этого элемента управления. Когда пользователь наводит указатель мыши на подчеркнутое слово, появляется всплывающее окно с дополнительной информацией и ссылка, которая открывает окно Web-браузера. Рис. 8. Пример использования элемента Popup Чтобы создать это окно, нужно включить в него элемент управления TextBlock с исходным текстом и элемент управления Popup с дополнительным содержимым, которое будет отображаться, когда пользователь наведет указатель мыши в нужное место. С технической точки зрения нет разницы в том, где определен тег Popup, поскольку он не связан ни с одним определенным элементом управления. Вместо этого необходимо определить свойства месторасположения элемента управления Popup. В 34 данном примере всплывающее окно появляется в текущей позиции указателя мыши: <TextBlock TextWrapping="Wrap">You can use a Popup to provide a link for a specific <Run TextDecorations="Underline" MouseEnter="run_MouseEnter" term</Run> of interest.</TextBlock> <Popup Name="popLink" StaysOpen="False" Placement="Mouse" MaxWidth="200" PopupAnimation="Slide" AllowsTransparency = "True"> <Border BorderBrush="Beige" BorderThickness="2" Background="White"> <TextBlock Margin="10" TextWrapping="Wrap" > For more information, see <Hyperlink NavigateUri="http://en.wikipedia.org/wiki/Term" Click="lnk_Click">Wikipedia</Hyperlink> </TextBlock> </Border> </Popup> В этом примере присутствуют два элемента, которых не были показаны ранее. Элемент Run позволяет применить форматирование к специфической части элемента управления TextBlock, Hyperlink позволяет задать текст, который может реагировать на щелчок кнопкой мыши, произведенный на нем. Также в рассматриваемом примере содержится сравнительно простой код, отображающий элемент управления Popup, когда мышь будет наведена на заданное слово, и код, открывающий Web-браузер при щелчке на ссылке: private void run_MouseEnter(object sender, MouseEventArgs e) { popLink.IsOpen = true; } private void lnk_Click(object sender, RoutedEventArgs e) { Process.Start(((Hyperlink)sender).NavigateUri.ToString()); } 35 2.6. Текстовые элементы управления WPF включает три текстовых элемента управления: TextBox, RichTextBox и PasswordBox. Элемент PasswordBox является прямым наследником класса Control. Элементы управления TextBox и RichTextBox являются наследниками класса TextBase. В отличие от рассмотренных выше элементов управления содержимым, текстовые окна могут заключать в себе только ограниченный тип содержимого. TextBox всегда хранит строку, определяемую в свойстве Text. PasswordBox тоже содержит строку текста, определяемую в свойстве Password), однако использует SecureString для защиты от некоторых типов атак. Только элемент RichTextBox может хранить содержимое более сложного порядка – FlowDocument, который может содержать в себе сложную комбинацию элементов. 2.6.1. Управление множеством строк текста Чаще всего элемент управления TextBox хранит одну строку текста, длину которой можно ограничить с помощью свойства MaxLength. Однако часто возникают случаи, когда приходится создавать многострочное текстовое окно для работы с большим объемом содержимого. Для этой цели следует воспользоваться свойством TextWrapping, присвоив ему значение Wrap или WrapWithOverflow. При значении Wrap текст будет всегда обрываться на краю элемента управления, даже если при этом слишком большое слово будет разбито на два, а WrapWithOverflow позволяет растянуть несколько строк за границы правого края, если алгоритм разрыва строки не может найти подходящее место для разбиения строки. Чтобы увидеть в текстовом окне множество строк, оно должно иметь подходящие размеры. Вместо того чтобы задавать жестко кодированную высоту, которая может вызвать проблемы с компоновкой, следует использовать свойства MinLines и MaxLines. Свойство MinLines определяет минимальное количество строк, которые должны отображаться в текстовом окне. Если этому свойству присвоить значение 2, то текстовое окно примет высоту, равную высоте минимум двух строк текста. Свойство 36 MaxLines задает максимальное количество отображаемых строк. Даже если текстовое окно будет расширено до таких размеров, чтобы уместиться в своем контейнере, оно не превысит заданный лимит. Отметим, что свойства MinLines и MaxLines не влияют на количество содержимого, которое можно поместить в текстовом окне. Они нужны для того, чтобы придать подходящие размеры текстовому окну. В коде можно проверить свойство LineCount, чтобы определить точно, сколько строк умещается в текстовом окне. Если текстовое окно поддерживает укладку текста, то нужно позаботиться о том, чтобы пользователь мог вводить больше текста, чем может быть отображено в видимых строках. По этой причине обычно добавляется линейка прокрутки, отображаемая постоянно или по запросу. Это достигается за счет присвоения свойству VerticalScrollBarVisibility значение visible или Auto. Отображение горизонтальной полосы прокрутки определяется свойством HorizontalScrollBarVisibility. Иногда приходится делать так, чтобы пользователь мог вводить жесткие возвраты в многострочном текстовом окне, нажимая клавишу <Enter>, т. к. обычно при нажатии клавиши <Enter> в текстовом окне активизируется кнопка, используемая по умолчанию. Чтобы текстовое окно поддерживало клавишу <Enter>, следует присвоить свойству AcceptsReturn значение true. Также можно задать свойство AcceptsTab, чтобы пользователь мог вставлять символы табуляции. В противном случае при нажатии клавиши <Таb> будет передан фокус следующему элементу управления, заданному в последовательности перехода с помощью клавиши табуляции. Класс TextBox включает также набор методов, которые позволяют программно перемещаться по текстовому содержимому небольшими или крупными шагами. К этим методам относятся LineUp(), LineDown(), PageUp(), PageDown(), ScrollToHome(), ScrollToEnd() и ScrollToLine(). Также возможна ситуация, когда необходимо будет создавать текстовые окна исключительно для отображения текста без возможности его редактирования. В этом случае свойству IsReadOnly необходимо присво37 ить значение true, чтобы исключить возможность редактирования текста. Это предпочтительнее блокирования текстового окна путем присваивания свойству IsEnabled значения false, поскольку заблокированное текстовое окно отображает текст, выделенный серым цветом, не поддерживает выделение текста (или копирование в буфер обмена) и его прокрутку. 2.6.2. Выделение текста Общеизвестно, что можно выделять текст в любом текстовом окне, щелкая кнопкой мыши и перемещая ее указатель, или, удерживая нажатой клавишу <Shift>, выделять его с помощью клавиш управления курсором. Класс TextBox дает возможность определять или изменять выделенный в данный момент текст программным образом, используя свойства SelectionStart, SelectionLength и SelectedText. Свойство SelectionStart определяет позицию, начиная с нуля, в которой будет осуществляться выделение текста. Например, если этому свойству присвоить значение 10, то первым выделенным символом будет одиннадцатый символ в текстовом окне. Свойство SelectionLength задает общее количество выделенных символов. Свойство SelectedText позволяет быстро проверить или изменить выделенный текст в текстовом окне. Можно отреагировать на изменение выделения с помощью события SelectionChanged. На рис. 9 показан пример окна, которое реагирует на это событие и отображает информацию о текущем выделении текста. Рис. 9. Элемент TextBox и выделение текста 38 Класс TextBox включает свойство AutoWordSelection, позволяющее управлять поведением выделения. Если ему присвоить значение true, то в текстовом окне будет выделяться по одному слову одновременно. 2.8. Другие возможности элемента управления TextBox Элемент управления TextBox обладает еще несколькими специализированными возможностями. Наиболее интересной является проверка орфографии, при которой нераспознанные слова подчеркиваются волнистой линией красного цвета. Пользователь может щелкнуть правой кнопкой мыши на нераспознанном слове и выбрать из списка правильный вариант, как показано на рис. 10. Рис. 10. Проверка орфографии в текстовом окне Чтобы добавить функцию проверки орфографии в элемент управления TextBox, нужно задать свойство зависимостей SpellCheck.IsEnabled, как показано ниже: <TextBox SpellCheck.IsEnabled="True"> Функция проверки орфографии является специфической для WPF и не зависит от любого другого установленного программного обеспечения (например, Office). Функция проверки определяет необходимый словарь на основании выбранного пользователем языка ввода на клавиатуре. Программно выбрать словарь можно с помощью свойства Language элемента управления TextBox или посредством задания атрибута xml:lang в элементе <TextBox>. 39 Другой полезной функцией TextBox является Undo, которая позволяет отменить последние изменения. Функцию Undo можно реализовать программно посредством метода Undo(), с помощью комбинации клавиш <Ctrl+Z>, при условии, что свойство CanUndo имеет значение True. Отметим, что при программных манипуляциях с текстом в текстовом окне можно использовать методы BeginChange() и EndChange(), чтобы сгруппировать серию действий, которые TextBox обработает как одно изменение. Впоследствии эти действия можно будет отменить за один раз. 2.9. Элемент управления PasswordBox Элемент управления PasswordBox выглядит подобно элементу управления TextBox, однако отображает строку, содержащую символыкружочки, скрывающие собой настоящие символы. Символы маски определяются свойством PasswordChar. Кроме того, PasswordBox не поддерживает работу с буфером обмена, поэтому нельзя скопировать текст, который содержится в этом элементе управления. По сравнению с TextBox, класс PasswordBox имеет более простой интерфейс. Как и класс TextBox, он предлагает свойство MaxLength, методы Clear(), Paste() и SelectAll(), а также событие PasswordChanged, которое возникает в случае изменения текста. Главное отличие этого элемента управления от TextBox состоит в реализации. Несмотря на то, что можно задать текст и прочитать его как обычную строку с помощью свойства Password, для внутреннего представления элемент управления PasswordBox использует исключительно объект System.Security.SecureString. SecureString — это исключительно текстовый объект, подобный обычной строке. Различие состоит в том, что SecureString хранится в памяти исключительно в зашифрованном виде. Ключ, который используется для расшифровки строки, генерируется псевдослучайным образом и хранится в области памяти, которая никогда не записывается на диск. Поэтому даже если произойдет поломка компьютера, злоумышленник не сможет проверить файл подкачки, чтобы извлечь данные пароля. В самом лучшем случае он найдет всего лишь зашифрованную форму. 40 Класс SecureString также имеет средство освобождения по запросу. При вызове метода SecureString.Dispose() автоматически или вручную, данные пароля, находящиеся в памяти, перезаписываются. Это дает гарантию, что вся информация о пароле будет стерта из памяти, и никто не сможет ею воспользоваться. 3. ЭЛЕМЕНТЫ УПРАВЛЕНИЯ СПИСКАМИ WPF включает многие элементы управления, которые могут работать с коллекцией элементов, начиная с простых элементов управления ListBox и ComboBox, которые будут рассмотрены в данном разделе, и заканчивая более специализированными элементами управления, такими как ListView, TreeView и ToolBar. Все эти элементы управления являются потомками класса ItemsControl. Класс ItemsControl является базовым для всех элементов управления, представляющих списки. Он предлагает два способа заполнения списка элементов. Наиболее простым способом является добавление элемента прямо в коллекцию Items, при котором используется код или XAML. Однако в WPF чаще всего применяется метод привязки данных. В этом случае свойству ItemsSource присваивается объект, имеющий коллекцию элементов данных, которые необходимо отобразить. Иерархия, которая начинается с ItemsControls, является несколько запутанной. Так, важная ветвь отведена селекторам, к которым относятся ListBox, ComboBox и TabControl. Эти элементы управления являются потомками класса Selector и имеют свойства, позволяющие отслеживать выделенный в данный момент времени элемент (SelectedItem) или его позицию (SelectedIndex). Отдельно от них определены элементы управления, связанные с работой со списками элементов, но не поддерживающие выделения. К ним относятся классы для меню, панелей инструментов и деревьев — все они представляют собой наследники ItemsControls, но не являются селекторами. 41 Чтобы использовать большинство возможностей любого элемента ItemsControl, необходимо использовать привязку данных. Это следует делать даже тогда, когда не производится выборка данных из базы или внешнего источника данных. Привязка данных в WPF обычно легко справляется с данными в различных формах, включая специальные объекты данных и коллекции. 3.1. Элемент управления ListBox Классы ListBox и ComboBox представляют два общих средства в среде Windows — списки переменной длины, которые дают пользователю возможность выбирать элемент. Следует отметить, что класс ListBox допускает множественный выбор, если его свойству SelectionMode присвоить значение Multiple или Extended. В режиме Extended пользователю необходимо удерживать нажатой клавишу <Ctrl>, чтобы выбрать дополнительные элементы, или клавишу <Shift>, чтобы выбрать диапазон элементов. В любом типе списка с множественным выбором для получения всех выделенных элементов используется коллекция SelectedItems вместо свойства SelectedItem. Чтобы добавить элементы в ListBox, можно вложить элементы ListBoxItem в элемент управления ListBox. Например, ниже показан элемент управления ListBox, содержащий список цветов: <ListBox> <ListBoxItem>Green</ListBoxItem> <ListBoxItem>Blue</ListBoxItem> <ListBoxItem>Yellow</ListBoxItem> <ListBoxItem>Red</ListBoxItem> </ListBox> Выше было сказано, что разные элементы управления обрабатывают вложенное содержимое по-разному. ListBox хранит каждый вложенный объект в своей коллекции Items. ListBox является достаточно гибким элементом управления. Он может хранить не только объекты ListBoxItem, но и любой произвольный 42 элемент. Это возможно благодаря тому, что класс ListBoxItem является наследником класса ContentControl, позволяющего хранить отдельную порцию вложенного содержимого. Если содержимое является классом, происходящим от UIElement, то оно будет визуализировано в элементе управления ListBox. Если оно представляет собой какой-либо иной тип объекта, ListBox вызовет метод ToString() и отобразит результирующий текст. Например, если нужно создать список с изображениями, можно использовать следующую разметку: <ListBox> <ListBoxItem> <Image Source="happyface. jpg"></Image> </ListBoxItem> <ListBoxItem> <Image Source="happyface. jpg"></Image> </ListBoxItem> </ListBox> ListBox способен автоматически создавать необходимые объекты ListBoxItem, поэтому можно разместить необходимые объекты прямо внутри элемента ListBox. Ниже представлен пример, в котором вложенные объекты StackPanel используются для комбинирования текста и изображений: <ListBox Margin="5" SelectionMode="Multiple" Name="lst" SelectionChanged="lst_SelectionChanged"> <StackPanel Orientation="Horizontal"> <Image Source="happyface.jpg" Width="30" Height="30"></Image> <Label VerticalContentAlignment="Center">A happy face</Label> </StackPanel> <StackPanel Orientation="Horizontal"> <Image Source="redx.jpg" Width="30" Height="30"></Image> <Label VerticalContentAlignment="Center">A warning sign</Label> </StackPanel> <StackPanel Orientation="Horizontal"> <Image Source="happyface.jpg" Width="30" Height="30"></Image> <Label VerticalContentAlignment="Center">A happy face</Label> </StackPanel> </ListBox> 43 В этом примере StackPanel становится элементом, встроенным в элемент управления ListBoxItem. Эта разметка создает расширенный список, показанный на рис. 11. Рис. 11. Пример расширенного списка ListBox Способность вкладывать произвольные элементы внутрь текстового окна дает возможность создавать разнообразные элементы управления списками, не используя при этом другие классы. Например, инструментальное средство Windows Forms включает класс CheckedListBox, отображаемый как список, в котором напротив каждого элемента ставится флажок. В WPF для реализации такого списка не нужен никакой специальный класс, поскольку можно сравнительно легко создать его с помощью стандартного элемента управления ListBox: <ListBox Name="lst" SelectionChanged="lst_SelectionChanged" CheckBox.Click="lst_SelectionChanged" > <CheckBox Margin="3">Option 1</CheckBox> <CheckBox Margin="3">Option 2</CheckBox> <CheckBox Margin="3">Option 3</CheckBox> </ListBox> Окно, содержащее элемент разметкой, представлено на рис. 12. 44 ListBox, определяемый данной Рис. 12. Элемент ListBox с элементами CheckBox При использовании списка, вмещающего в себя разные элементы, следует иметь в виду, что когда считывается значение SelectedItem (а также коллекции SelectedItems и Items), объекты ListBoxItem не видны — вместо них видны объекты, помещенные в список. В примере с элементом управления CheckedListBox это означает, что SelectedItem представляет объект CheckBox. Ниже показан код, который реагирует на возникновение события SelectionChanged. Принцип его действия следующий: он получает выделенный в данный момент CheckBox и показывает, был ли этот элемент отмечен: private void lst_SelectionChanged(object sender, RoutedEventArgs e) { // Select when checkbox portion is clicked (optional). if (e.OriginalSource is CheckBox) { lst.SelectedItem = e.OriginalSource; } if (lst.SelectedItem == null) return; txtSelection.Text = String.Format( "You chose item at position {0}.\r\nChecked state is {1}.", lst.SelectedIndex, ((CheckBox)lst.SelectedItem).IsChecked); 45 } В следующем фрагменте кода происходит циклический перебор коллекции элементов, что позволяет узнать, какие из них были отмечены: private void cmd_CheckAllItems(object sender, RoutedEventArgs e) { StringBuilder sb = new StringBuilder(); foreach (CheckBox item in lst.Items) { if (item.IsChecked == true) { sb.Append( item.Content + " is checked."); sb.Append("\r\n"); } } txtSelection.Text = sb.ToString(); } Окно списка, в котором используется этот код, показано на рис. 12. Помещая вручную элементы в список, необходимо решить, будут ли элементы помещены «как есть», или же их нужно упаковать в объект ListBoxItem. Второй подход часто является понятным, хотя и более трудоемким. Здесь важно быть последовательным. Например, если поместить объекты StackPanel в список, то объектом ListBox.SelectedItem будет StackPanel. Если поместить объекты StackPanel, упакованные объектами ListBoxItem, то объектом ListBox.SelectedItem будет ListBoxItem. ListBoxItem обладает еще одной особенностью: в нем определено свойство IsSelected, значение которого можно считывать (или устанавливать), и события Selected и Unselected, которые информируют о том, когда был выделен данный элемент. Однако эти же возможности можно реализовать с помощью членов класса ListBox, таких как свойство SelectedItem (или SelectedItems) и событие SelectionChanged. 46 3.2. Элемент управления ComboBox Элемент управления ComboBox подобен элементу управления ListBox. Он хранит коллекцию объектов ComboBoxItem, которые создаются явным или неявным образом. Как и ListBoxItem, ComboBoxItem является элементом управления содержимым, который может хранить любой вложенный элемент. Ключевым отличием классов ComboBox и ListBox является способ их визуализации в окне. Элемент управления ComboBox использует раскрывающийся список, а, следовательно, пользователь может выбрать только один элемент за один раз. Если нужно сделать так, чтобы пользователь мог вводить текст в комбинированном окне для выбора элемента, следует присвоить свойству IsEditable значение true. Кроме того, необходимо убедиться, что хранятся обычные, только текстовые объекты ComboBoxItem, или объект, обеспечивающий значащее представление функцией ToString(). Например, если заполнить редактируемое комбинированное окно объектами Image, то текст, который появится в верхней части, будет представлять полностью определенное имя класса Image. Элемент управления ComboBox имеет одно ограничение в способе подгонки размеров при автоматическом их выборе. ComboBox выбирает для себя такую ширину, которая позволит уместить его содержимое. Это означает, что когда пользователь будет переходить от одного элемента к другому, ComboBox будет выбирать подходящий размер. Не существует способа заставить ComboBox принять размер наибольшего элемента. Вместо этого нужно задать жестко закодированное значение свойства Width, что не является идеальным решением. 4. ЭЛЕМЕНТЫ УПРАВЛЕНИЯ, ОСНОВАННЫЕ НА ДИАПАЗОНАХ ЗНАЧЕНИЙ WPF включает три элемента управления, использующих концепцию диапазонов значений. Эти элементы принимают числовое значение, которое находится в диапазоне между заданными минимальным и максималь- 47 ным значениями. Эти элементы управления— ScrollBar, ProgressBar и Slider — являются наследниками класса RangeBase. Однако, несмотря на то, что они вместе используют одну абстракцию (диапазон), работают они по-разному. Класс RangeBase определяет свойства, перечисленные в таблице 4. Таблица 4 Свойства класса RangeBase Имя Value Описание Текущее значение элемента управления (которое должно находиться в диапазоне между минимумом и максимумом). По умолчанию оно начинается с 0. Значение Value имеет тип double, что дает ему возможность принимать дробные значения. Изменение значения порождает событие ValueChanged Maximum Верхний лимит (максимальное допустимое значение) Minimum Нижний лимит (минимальное допустимое значение) SmallChange Величина, на которую уменьшается или увеличивается значение свойства Value при «малом изменении». Для элементов управления ScrollBar и Slider это величина, на которую изменяется значение, когда пользователь использует клавиши управления курсором LargeChange Величина, на которую уменьшается или увеличивается значение свойства Value в «крупном изменении». Назначение крупного изменения зависит от элемента управления. Для элементов управления ScrollBar и Slider это та величина, на которую изменяется значение, когда пользователь использует кнопки <Page Up> и <Раgе Down>, или когда щелкает на линейке в любой стороне ползунка Как правило, элемент управления ScrollВаr не используется напрямую. Чаще всего применяется элемент управления более высокого уровня ScrollViewer, который обладает свойствами двух элементов управления ScrollBar. Однако по своей сути более ценными являются Slider и ProgressBar. 48 4.1. Элемент управления Slider Элемент управления Slider является более специализированным элементом управления, который может оказаться полезным. Например, им можно воспользоваться для задания числовых значений в тех ситуациях, когда важным является не само число, его относительное значение. Например, громкость в проигрывателе лучше всего устанавливать, перемещая в разные стороны бегунок на линейке прокрутки. Общая позиция бегунка показывает относительную громкость (нормально, тихо, громко), а лежащее в его основе число часто не представляет особого смысла для пользователя. Ключевые свойства элемента управления Slider определены в классе RangeBase. Кроме них, можно использовать все свойства, перечисленные в таблице 5. Таблица 5 Дополнительные свойства класса Slider Имя Описание Orientation Переключает между вертикальным и горизонтальным ползунком Delay и Interval Управляет скоростью перемещения ползунка вдоль дорожки, когда пользователь щелкает и удерживает нажатой клавишу мыши на стороне ползунка. Оба значения задаются в миллисекундах. Delay — это время, по истечении которого ползунок переместится на одну единицу (малое изменение) после щелчка, а Interval — время, которое должно истечь, прежде чем он продолжит перемещение, если продолжить удерживать нажатой кнопку мыши Delay и Interval TickPlacement Определяет место появления отметок возле линейки, которые помогают визуализировать шкалу. По умолчанию свойство TickPlacement имеет значение None, вследствие чего ни одна отметка не отображается. Если выполняется работа с горизонтальным ползунком, можно поместить метки над дорожкой (TopLeft) или под ней (BottomRight). Если выполняется работа с вертикальным ползунком, 49 Окончание таблицы 5 Имя TickPlacement Описание можно поместить их слева (TopLeft) или справа (BottomRight) TickFrequency Задает интервал между отметками, который определяет, сколько отметок будет появляться. Например, можно помещать их через каждые 5 числовых единиц, каждые 10 и т.д. Ticks Если необходимо поместить отметки в определенных, нерегулярных позициях, можно использовать коллекцию Ticks. Достаточно просто добавить в эту коллекцию по одному числу (типа double) для каждой отметки IsSnapToTick Если это свойство будет иметь значение true, то при перемещении ползунка он автоматически будет переведен в место, прыгая к ближайшей отметке. По умолчанию это свойство имеет значение false IsSelectionRangeEnabled Если этому свойству будет присвоено значение true, то можно использовать диапазон для затенения участка линейки прокрутки. Диапазон выбора позиции задается с помощью свойств SelectionStart и SelectionEnd. Диапазон выбора не имеет значения, тем не менее, можно использовать его для любых целей Ниже приведен пример разметки, демонстрирующей различные варианты использования элементов управления Slider: <StackPanel Margin="10"> <TextBlock Margin="0,0,0,5">Normal Slider (Max=100, Val=10)</TextBlock> <Slider Maximum="100" Value="10"></Slider> <TextBlock Margin="0,15,0,5">Slider with Tick Marks (TickFrequency=10, TickPlacement=BottomRight)</TextBlock> <Slider Maximum="100" Value="10" TickFrequency="10" TickPlacement="BottomRight"></Slider> <TextBlock Margin="0,15,0,5">Slider with Irregular Tick Marks (Ticks=0,5,10,15,25,50,100)</TextBlock> <Slider Maximum="100" Value="10" Ticks="0,5,10,15,25,50,100" TickPlacement="BottomRight"></Slider> 50 <TextBlock Margin="0,15,0,5" TextWrapping="Wrap">Slider with a Selection Range (IsSelectionRangeEnabled=True, SelectionStart=25, SelectionEnd=75)</TextBlock> <Slider Maximum="100" Value="10" TickFrequency="10" TickPlacement="BottomRight" IsSelectionRangeEnabled="True" SelectionStart="25" SelectionEnd="75"></Slider> </StackPanel> На рис. 13 показан внешний вид окна, определяемого данной разметкой: Рис. 13. Различные варианты использования ползунков 4.2. Элемент управления ProgressBar Элемент управления ProgressBar показывает ход выполнения длительной задачи. В отличие от ползунка, ProgressBar не является интерактивным элементом управления. Более того, за изменение значения свойства Value отвечает исключительно программный код. ProgressBar не имеет предварительно заданной высоты, поэтому разработчику необходимо самостоятельно установить свойство Height или поместить элемент управления в подходящий контейнер с фиксированными размерами. Элемент управления ProgressBar может служить для отображения «долгоиграющего» индикатора состояния, даже если заранее не известно, насколько долго будет длиться задача. Это делается путем присваивания свойству IsIndeterminate значения true: <ProgressBar Height="10" IsIndeterminate="True"></ProgressBar> После того как свойство IsIndeterminate задано соответствующим образом, свойства Minimum, Maximum и Value больше не будут иметь 51 значения, так как ProgressBar будет показывать периодический зеленый импульс, перемещающийся слева направо, что является универсальным способом Windows отображения хода выполнения какой-либо задачи. Эта разновидность индикатора имеет определенный смысл в панели состояния приложения. Например, можно применять его для того, чтобы показать, что происходит выполнение какого-либо длительного процесса, например соединение с удаленным сервером для передачи информации. 5. ОКНА Окна являются основными элементами в любом настольном приложении. И хотя в WPF имеется модель для создания навигационных приложений, распределяющих задачи по отдельным страницам, окна все равно остаются преобладающей технологией для создания приложений. 5.1. Класс Window Класс Window является наследником класса ContentControl. Это означает, что он может содержать только одного потомка, каковым обычно является контейнер компоновки, чаще всего Grid, и что его фон можно закрашивать с помощью кисти путем установки свойства Background. Можно также использовать свойства BorderBrush и BorderThickness для добавления вокруг окна границы, но эта граница добавляется внутри оконной рамки. Также класс Window имеет небольшой набор свойств и методов. Наиболее очевидными из них являются свойства, которые касаются внешнего вида и позволяют изменять способ отображения неклиентской части окна. Основные свойства класса Window перечислены в таблице 6. Таблица 6 Основные свойства класса Window Имя AllowsTransparency Описание Если для свойства AllowsTransparency устанавливается значение true, класс Window позволяет другим окнам «прог- 52 Продолжение таблицы 6 Имя Описание лядывать» через данное окно при условии, что для фона установлен прозрачный цвет. Если для него устанавливается значение false (значение по умолчанию), находящееся позади данного окна содержимое не «проглядывается», и прозрачный цвет фона визуализируется как черный. Это свойство в случае использования вместе с имеющим значение None свойством WindowsStyle позволяет создавать окна, имеющие необычную форму Icon Это свойство представляет объект ImageSource, указывающий на пиктограмму, которая должна использоваться для данного окна. Пиктограммы отображаются в левом верхнем углу окна, в панели задач (если для свойства ShowInTaskBar установлено значение true) и в окне выбора, которое появляется, когда пользователь нажимает комбинацию клавиш <Alt+Tab> для перехода из одного работающего приложения в другое. Поскольку эти пиктограммы имеют разный размер, в используемом для них файле .ico должны содержаться изображения размером как минимум 16*16 и 32*32 пикселя. Если в свойстве icon содержится ссылка null, окно получает ту же пиктограмму, что и приложение. Если значение свой- Icon ства не определено, WPF использует стандартную пиктограмму Тор и Left Эти свойства определяют расстояние между левым верхним углом окна и левыми верхними краями экрана в аппаратно независимых пикселях. В случае изменения любого из них вызывается событие LocationChanged. Если для свойства WindowStartupPosition задается значение Manual, эти свойства могут устанавливаться разработчиком до появления окна для определения его позиции. Их также еще можно использовать для изменения позиции окна после того, как оно уже появилось ResizeMode Это свойство берет значение из перечисления ResizeMode, которое определяет, может ли пользователь изменять размер окна. Этот параметр также еще влияет на видимость кнопок разворачивания и сворачивания окна. Чтобы полностью 53 Продолжение таблицы 6 Имя Описание заблокировать окно, следует использовать значение NoResize, чтобы разрешить только сворачивать окно — значение CanMinimize, чтобы разрешить все касающиеся изменения размера окна действия — значение CanResize, а чтобы добавить визуальную подсказку, появл яющуюся в правом нижнем углу окна и показывающую, что размер окна можно изменять — значение CanResizeWithGrip RestoreBounds Это свойство извлекает информацию о границах окна. Однако если окно в текущий момент находится в развернутом или свернутом состоянии, в этом свойстве отображаются те границы, которые использовались последними перед тем, как окно было развернуто или свернуто ShowInTaskbar Если для этого свойства устанавливается значение true, окно отображается в панели задач и списке, появляющемся после нажатия комбинации клавиш <Alt+Tab>. Обычно значение true для этого свойства устанавливается только для главного окна приложения SizeToContent Это свойство позволяет создавать окно, способное автоматически увеличиваться в соответствии с размером содержимого. Чтобы отключить автоматическое изменение размеров окна, следует использовать значение Manual, а чтобы разрешить окну увеличиваться в различных направлениях в соответствии с размерами динамического содержимого — значение Height, Width или WidthAndHeight Title В этом свойстве указывается заголовок, который должен отображаться в строке заголовка окна и в панели задач Topmost Когда для этого свойства устанавливается значение true, данное окно всегда отображается поверх всех остальных окон в приложении. Это очень удобный параметр для палитр, которые обычно должны располагаться поверх других окон. От- Topmost метим, что окна системных программ (например, диспетчера задач) будут располагаться поверх окна приложения, вне зависимости от установленного свойства WindowStartupLocation Это свойство берет значение из перечисления WindowStartupLocation. Для размещения окна именно в той 54 Окончание таблицы 6 Имя Описание позиции, которая указывается в свойствах Left и Тор, следует использовать значение Manual, для размещения окна по центру экрана — значение CenterScreen, а для размещения окна с учетом позиции того окна, которое его запустило — значение CenterOwner. В случае отображения немодального окна с помощью CenterOwner нужно обязательно удостовериться в том, что свойство Owner нового окна устанавливается перед отображением данного WindowState Это свойство берет значение из перечисления WindowState. Оно информирует о том, в каком состоянии находится окно в текущий момент: в развернутом, свернутом или обычном, а также позволяет изменять это состояние WindowStyle Это свойство берет значение из перечисления WindowStyle, которое определяет границу окна. Допустимыми значениями являются: SingleBorderWindow, ThreeDBorderWindow (визуализирует одинаковые границы в Windows Vista, Windows 7 и почти одинаковые в Windows ХР), ToolWindow и None. Различия между ними показаны на рис. 14 Рис. 14. Различные значения свойства WindowStyle О событиях жизненного цикла, которые вызываются при создании, активизации или выгрузке окна, уже рассказывалось в [4]. Помимо них класс Windows также включает события LocationChanged и Win- 55 dowStateChanged, которые вызываются при изменении позиции и состояния окна соответственно. 5.2. Отображение окна Чтобы отобразить окно, необходимо создать экземпляр класса Window и вызвать метод Show() или ShowDialog(). Метод ShowDialog() отображает модальное окно. Модальные окна не позволяют пользователю получать доступ к родительскому окну, блокируя возможность использования в нем мыши и возможность ввода в нем каких-либо данных до тех пор, пока модальное окно не будет закрыто. К тому же метод ShowDialog() не осуществляет возврат до тех пор, пока модальное окно не будет закрыто, поэтому выполнение любого находящего после него кода на время откладывается. Отметим, что это не означает, что при открытом модальном окне не может выполняться и никакой другой код, например, параллельно исполняемый поток кода все равно будет работать. Наиболее часто применяемая в коде схема выглядит следующим образом: отображение модального окна, ожидание его закрытия и последующее выполнение над его данными какой-нибудь операции. Ниже показан пример использования метода ShowDialog(): TaskWindow winTask = new TaskWindow(); winTask.ShowDialog(); // Выполнение достигает этой точки после закрытия winTask. Метод Show() отображает немодальное окно, которое не блокирует доступ пользователя ни к каким другим окнам. Также следует учитывать, что метод Show() осуществляет возврат сразу же после отображения окна, так что следующие после него в коде операторы выполняются незамедлительно. Можно создавать и показывать сразу несколько немодальных окон, и пользователь может взаимодействовать со всеми ними одновременно. В случае применения нескольких немодальных окон иногда требуется код синхронизации, гарантирующий синхронность изменений и адекватность информации во всех окнах, исключая вероятность работы пользователя с недействительными данными. 56 Ниже показан пример использования метода Show(): MainWindow winMain = new MainWindow(); winMain.Show(); // Выполнение достигает этой точки сразу же после отображения winMain. Модальные окна идеально подходят для предоставления пользователю приглашения сделать выбор, прежде чем выполнение операции сможет быть продолжено. В качестве примера рассмотрим приложение Microsoft Word. Это приложение всегда отображает окна Options (Параметры) и Print (Печать) в модальном режиме, вынуждая пользователя принимать решение перед продолжением. С другой стороны, окна, предназначенные для поиска по тексту или проверки наличия в документе орфографических ошибок, Microsoft Word отображает в немодальном режиме, позволяя пользователю редактировать текст в основном окне документа, пока идет выполнение задачи. Закрывается окно с помощью метода Close(). Альтернативным вариантом является сокрытие окна из вида путем использования метода Hide() или установки для свойства Visibility значения Hidden. И в том и в другом случае окно остается открытым и доступным для кода. Как правило, скрывать имеет смысл только немодальные окна, поскольку что при сокрытии модального окна код остается в «замороженном» состоянии до тех пор, пока окно не будет закрыто, а закрыть невидимое окно пользователь никак не сможет. 5.3. Позиционирование окна Чаще всего размещать окно в каком-нибудь точно определенном месте на экране не требуется. В таких случаях можно просто использовать для свойства WindowState значение CenterOwner. В более редких случаях требуется указывать точную позицию окна, что подразумевает использование значения Manual для свойства WindowState и указание точных координат в свойствах Left и Right. Иногда выбору подходящего месторасположения и размера для окна нужно уделять больше внимания. В качестве примера рассмотрим следу- 57 ющую ситуацию: случайно было создано окно размером, который является слишком большим для отображения на дисплее с низким разрешением. Если речь идет о приложении с одним единственным окном, тогда наилучшим решением будет создание окна с возможностью изменения размеров. Если же речь идет о приложении с несколькими плавающими окнами, то дело усложняется. Можно попробовать просто ограничить позиции окна теми, которые поддерживаются даже на самых маленьких мониторах, но такое решение, скорее всего, будет неприемлемо для пользователей мониторов с большей разрешающей способностью. Поэтому вероятнее всего придется принимать решение о наилучшем размещении окна во время выполнения. А для этого потребуется извлечь базовую информацию о доступном экранном оборудовании с помощью класса System.Windows.SystemParameters. Класс SystemParameters состоит из большого списка статических свойств, которые возвращают информацию о различных параметрах системы. Например, его можно использовать для определения, включил ли пользователь помимо всего прочего функцию «горячего» отслеживания (hot tracking) и возможность перетаскивания целых окон. В случае окон класс SystemParameters является особенно полезным, потому что предоставляет два свойства, которые возвращают информацию о размерах текущего экрана: свойство FullPrimaryScreenHeight и свойство FullPrimaryScreenWidth. Оба они довольно просты, что иллюстрирует показанный ниже код, выполняющий центрирование окна во время выполнения: double screenHeight = SystemParameters.FullPrimaryScreenHeight; double screenWidth = SystemParameters.FullPrimaryScreenWidth; this.Top = (screenHeight - this.Height) / 2; this.Left = (screenWidth - this.Width) / 2; Хотя этот код и эквивалентен применению свойства windowState со значением CenterScreen, он предоставляет гибкость, позволяя реализовать различную логику позиционирования и выполнять ее в подходящее время. Второй вариант — воспользоваться прямоугольником SystemParameters.WorkArea для размещения окна в доступной области экрана. При вы- 58 числении рабочей области не учитывается область, в которой пристыковываются панель задач и любые другие панели инструментов, стыкованные с рабочим столом: double workHeight = SystemParameters.WorkArea.Height; double workWidth = SystemParameters.WorkArea.Width; this.Top = (workHeight - this.Height) / 2; this.Left = (workWidth - this.Width) / 2; Можно отметить, что оба варианта обладают одним небольшим недостатком. Когда свойство Тор устанавливается для окна, которое уже является видимым, это окно незамедлительно перемещается и обновляется. То же происходит и при установке свойства Left в следующей строке кода. В результате пользователям может показаться, что окно перемещается дважды. Проблема состоит в том, что класс Window не предоставляет метода, который бы позволял устанавливать оба этих свойства одновременно. Поэтому единственным решением является позиционирование окна после его создания, но перед его отображением с помощью метода Show() или ShowDialog(). 5.4. Манипулирование информацией о местоположении окна К числу типичных требований для окна относится и запоминание его последнего месторасположения. Эта информация может храниться как в конфигурационном файле пользователя, так и в системном реестре Windows. Если есть необходимость сделать так, чтобы информация о расположении какого-то важного окна хранилась в конфигурационном файле конкретного пользователя, то необходимо выполнить два следующих шага: нужно дважды щелкнуть на узле Properties (Свойства) в окне проводника решений Solution Explorer и выбрать раздел Settings (Параметры); 59 добавить действующий только на уровне данного пользователя параметр с типом данных System.Windows.Rect. Соответствующее окно показано на рис. 15: Рис. 15. Свойство для хранения размера и положения окна При наличии такого параметра далее достаточно просто создать код, который будет автоматически сохранять информацию о размерах и расположении окна: Properties.Settings.Default.WindowPosition = win.RescoreBounds; Properties.Settings.Default.Save(); Отметим, что в приведенном коде используется свойство RescoreBound, которое предоставляет правильные размеры (т.е. последний размер окна в обычном — не свернутом и не развернутом — состоянии), даже если в текущий момент окно развернуто или свернуто. Извлечь эту информацию тоже достаточно просто: try { Rect bounds = Properties.Settings.Default.WindowPosition; win.Top = bounds.Top; win.Left = bounds.Left; if (win.SizeToContent == SizeToContent.Manual) { win.Width = bounds.Width; win.Height = bounds.Height; } } catch { 60 MessageBox.Show("Нет сохраненных размеров"); } Единственным недостатком такого подхода является необходимость создавать отдельное свойство для каждого окна, у которого должна сохраняться информация о расположении и размерах. Если требуется, чтобы информация о расположении сохранялась у множества различных окон, тогда, возможно, лучше будет разработать более гибкую систему. Например, ниже показан вспомогательный класс, который сохраняет информацию о расположении для любого передаваемого ему окна с помощью ключа реестра, хранящего имя этого окна. public class WindowPositionHelper { public static string RegPath = @"Software\MyApp\"; public static void SaveSize(Window win) { // Create or retrieve a reference to a key where the settings // will be stored. RegistryKey key; key = Registry.CurrentUser.CreateSubKey(RegPath + win.Name); key.SetValue("Bounds", win.RestoreBounds.ToString()); } public static void SetSize(Window win) { RegistryKey key; key = Registry.CurrentUser.OpenSubKey(RegPath + win.Name); if (key != null) { Rect bounds = Rect.Parse(key.GetValue("Bounds").ToString()); win.Top = bounds.Top; win.Left = bounds.Left; // Only restore the size for a manually sized // window. if (win.SizeToContent == SizeToContent.Manual) { win.Width = bounds.Width; 61 win.Height = bounds.Height; } } } } Чтобы использовать этот класс в окне, нужно вызвать метод SaveSize() при закрытии окна и метод SetSize() при его первом открытии. В каждом случае также необходимо передать ссылку на окно, которое вспомогательный класс должен инспектировать. Также следует обратить внимание на то, что у каждого окна должно быть свое значение свойства Name. Для появления возможности различения окон с одинаковыми именами класс может быть дополнен для использования дополнительной идентификационной информации. 5.5. Владение окнами .NET позволяет окну «владеть» другими окнами. Окна, имеющие окно-владельца, удобно применять для плавающих окон панелей инструментов и окон команд. Одним из примеров такого окна является окно Find and Replace (Найти и заменить) в Microsoft Word. Когда окно-владелец сворачивается, окно, которым оно владеет, тоже автоматически сворачивается. Когда имеющее владельца окно перекрывает окно, которое им владеет, оно всегда отображается сверху. Для поддержки владения окна класс Window предлагает два свойства: свойство Owner и свойство OwnedWindows. Свойство Owner представляет собой ссылку, которая указывает на окно, владеющее текущим окном, а свойство OwnedWindows — коллекцию всех окон, которыми владеет текущее окно. Настройка владения окна подразумевает просто установку свойства Owner, как показано ниже: // Создать новое окно. ToolWindow winTool = new ToolWindow(); // Обозначить текущее окно как являющееся владельцем. winTool.Owner = this; 62 // Показать окно, принадлежащее окну-владельцу. winTool.Show(); Окна, имеющее окно-владельца, всегда отображаются как немодальные. Чтобы удалить такое окно, нужно всего лишь установить для его свойства Owner значение null. Следует отметить тот факт, что WPF не включает системы для построения многодокументных приложений (Multiple Document Interface — MDI). При необходимости в более сложной системе управления окнами, разработчику придется реализовывать ее самостоятельно. Окно, имеющее владельца, может само владеть каким-нибудь другим окном, которое, в свою очередь, может владеть еще каким-нибудь окном и т.д. Единственным ограничением является то, что окно не может владеть самим собой, а также то, что два окна не могут владеть друг другом. 5.6. Модель диалогового окна Часто отображая окно как модальное, разработчик предлагает пользователю сделать какой-нибудь выбор. Код, отображающий окно, дожидается получения результата этого выбора и затем выполняет на его основании соответствующее действие. Такой дизайн называется моделью диалогового окна, а само отображаемое модальное окно — диалоговым окном. Такой дизайн можно легко корректировать путем создания в диалоговом окне общедоступного свойства. Это свойство может устанавливаться, когда пользователь делает в диалоговом окне выбор. Далее диалоговое окно может закрываться, а отображавший его код — проверять установленное для свойства значение и на его основании определять, какое действие должно быть выполнено следующим. Такая инфраструктура уже отчасти жестко закодирована в классе Window. У каждого окна имеется свойство DialogResult, которое может принимать значение true, false или null. Обычно значение true означает, что пользователь выбрал продолжить операцию (например, щелкнул на кнопке ОК). а значение false— что он отменил операцию. 63 Лучше всего, когда результаты отображения диалогового окна возвращаются в вызывающий код в виде значения, возвращаемого методом ShowDialog(). Это позволяет создавать, отображать и анализировать результат, возвращаемый диалоговым окном, с помощью всего лишь такого короткого кода: DialogWindow dialog = new DialogWindow(); if (dialog.ShowDialog() == true) { // Пользователь разрешил действие. } else { // Пользователь отменил действие. } Также существует и еще один короткий путь. Вместо ручной установки свойства DialogResult после выполнения пользователем щелчка на кнопке, можно назначить кнопку кнопкой Accept (Принять) путем установки для свойства IsDefault значения true. Тогда щелчок на этой кнопке будет автоматически приводить к установке для свойства DialogResult значения true. Подобным образом также можно сделать кнопку кнопкой Cancel (Отмена), в результате чего щелчок на ней будет автоматически приводить к установке для свойства DialogResult значения Cancel. 5.7. Встроенные диалоговые окна Операционная система Windows включает много встроенных диалоговых окон, доступ к которым можно получать через API-интерфейс Windows. Для некоторых из них WPF предоставляет классы-упаковщики, из которых чаще всего используется класс System.Windows.MessageBox, который предоставляет статический метод Show(). Этот код можно использовать для отображения стандартных окон сообщений Windows. Ниже показана наиболее распространенная перегруженная версия этого метода: 64 MessageBox.Show("Требуется ввести имя", "Ошибка при вводе имени", MessageBoxButton.OK, MessageBoxImage.Exclamation); Перечисление MessageBoxButton позволяет выбирать кнопки, которые должны отображаться в окне сообщений. К числу доступных вариантов относятся ОК, OKCancel, YesNo и YesNoCancel. Перечисление MessageBoxImage позволяет выбирать пиктограмму для окна сообщения (Information, Exclamation, Error, Hand, Question, Stop и т. д.). Для класса MessageBox в WPF предусмотрена специальная поддержка печатных функций, подразумевающих использование класса PrintDialog, а также классов OpenFileDialog и SaveFileDialog в пространстве имен Microsoft.Win32. Классы OpenFileDialog и SaveFileDialog имеют некоторые дополнительные функциональные возможности, часть которых наследуются от класса FileDialog. Оба из них поддерживают строку фильтра, которая устанавливает разрешенные расширения файлов. Класс OpenFileDialog также предлагает свойства, которые позволяют проверять выбор пользователя (CheckFileExists) и предоставлять возможность выбрать сразу несколько файлов (Multiselect). Ниже показан пример кода, который отображает диалоговое окно OpenFileDialog и выбранные файлы в окне списка после закрытия этого диалогового окна. OpenFileDialog myDialog = new OpenFileDialog(); myDialog.Filter = "Image Files(*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF" + "All files (*.*) |*.*"; myDialog.CheckFileExists = true; myDialog.Multiselect = true; if (myDialog.ShowDialog() == true) { lstFiles.Items.Clear(); foreach (string file in myDialog.FileNames) { lstItems.Items.Add(file); } } 65 5.8. Непрямоугольные окна Окна необычной формы часто являются товарным знаком современных популярных приложений, в частности, редакторов фотографий, программ для создания кинофильмов и МРЗ-проигрывателей. Скорее всего, в WPF-приложениях они будут встречаться даже еще более часто. В создании базового приложения нестандартной формы в WPF нет ничего сложного. Однако создание привлекательного профессионально выглядящего окна необычной формы требует немалых усилий — и часто привлечения талантливого дизайнера графики для создания набросков и фоновой графики. 5.8.1. Простое окно нестандартной формы Базовая процедура для создания окна нестандартной формы подразумевает выполнение следующих шагов: 1. Установить для свойства Window.AllowsTransparency значение true. 2. Установить для свойства Window.WindowStyle значение None, чтобы скрыть неклиентскую область окна. Если этого не сделать, при попытке отображения окна появится ошибка InvalidOperationException. 3. Установить для фона окна прозрачный цвет или задать в качестве фонового изображения некоторое изображение, имеющее прозрачные области. Эти три шага эффективно удаляют стандартный внешний вид окна. Для обеспечения эффекта окна необычной формы необходимо создать какое-то непрозрачное содержимое, имеющее нужную форму. Здесь возможны различные варианты: предоставить фоновую графику, используя файл формата, поддерживающего прозрачность. Например, для фона можно использовать файл PNG. Этот подход прост и прямолинеен, однако он обладает существенным недостатком: из-за того, что окно будет визуализироваться с большим количеством пикселей и более 66 высокими системными параметрами DPI фоновая графика может приобрести искаженный вид. Также могут возникнуть сложности в случае разрешения пользователю изменять размеры окна; использовать доступные в WPF функции для рисования формы, чтобы создать фон с векторным содержимым. Такой подход исключает потерю качества, какими бы ни были размеры окна и DPIпараметры системы. Однако в этом случае вероятнее всего потребуется использовать средство проектирования, поддерживающее XAML. Если необходима интеграция с Visual Studio, то наилучшим вариантом является использование среды Expression Blend. Отметим, что даже традиционные приложения, например Adobe Illustrator, могут предлагать функции для экспорта XAML через подключаемый модуль; использовать более простой WPF – элемент, имеющий необходимую форму. Например, окно со скругленными краями можно создать с помощью элемента Border. Такой подход позволяет создавать окна с современным внешним видом в стиле Office без применения каких-либо дизайнерских навыков. Ниже в качестве примера приведен код создания пустого прозрачного окна с применением первого подхода и предоставлением файла PNG для прозрачных областей. <Window x:Class="Windows.TransparentBackground" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Windows" Height="300" Width="300" WindowStyle="None" AllowsTransparency="true" MouseLeftButtonDown="window_MouseLeftButtonDown"> <Window.Background> <ImageBrush ImageSource="shapes.png"></ImageBrush> </Window.Background> <Grid> <Grid.RowDefinitions> <RowDefinition></RowDefinition> 67 <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Button Margin="20">A Sample Button</Button> <Button Margin="20" Grid.Row="2" Click="cmdClose_Click">Close</Button> </Grid> </Window> На рис. 16 показано окно, определяемое приведенной разметкой. Это окно необычной формы, состоящее из окружности и квадрата, имеет не только прозрачные части, сквозь которые может просматриваться находящееся за ним содержимое, но и кнопки, которые выходят за границы изображения и накладываются на прозрачную область, из-за чего кажется, будто бы они существуют сами по себе, без окна. Рис. 16. Окно необычной формы На рис. 17 показано другое, более простое окно необычной формы. В этом окне используется элемент Border со скругленными углами для придания окну простого и в то же время нестандартного внешнего вида. Компоновка также является упрощенной, поскольку исключает случайный выход содержимого за пределы границы, а размер границы может легко изменяться без наличия элемента Viewbox. 68 Рис. 17. Окно, форма которого определяется элементом Border В этом окне содержится элемент Grid с тремя строками, которые используются для организации строки заголовка, строки нижнего колонтитула и размещаемого между ними содержимого. В строке с содержимым находится еще один элемент Grid, который имеет другой фон и может содержать другие необходимые элементы (в рассматриваемом примере в нем находится только единственный элемент TextBlock). Ниже показан код разметки, с помощью которого создается такое окно: <Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525" AllowsTransparency="True" WindowStyle="None" Background="Transparent"> <Border Width="Auto" Height="Auto" Name="windowFrame" BorderBrush="#395984" BorderThickness="1" CornerRadius="0,20,30,40" > <Border.Background> <LinearGradientBrush> <GradientBrush.GradientStops> <GradientStopCollection> <GradientStop Color="#E7EBF7" Offset="0.0"/> <GradientStop Color="#CEE3FF" Offset="0.5"/> </GradientStopCollection> </GradientBrush.GradientStops> </LinearGradientBrush> 69 </Border.Background> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <TextBlock Text="Title Bar" Margin="l" Padding="5"></TextBlock> <Grid Grid.Row="l" Background="#B5CBEF"> <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="White" FontSize="20">Content Goes Here</TextBlock> </Grid> <TextBlock Grid.Row="2" Text="Footer" Margin="l,10,1,1" Padding="5" HorizontalAlignment="Center"></TextBlock> </Grid> </Border> </Window> Для завершения внешнего вида этого окна следует создать кнопки, имитирующие размещаемые в правом верхнем углу стандартные кнопки для разворачивания, сворачивания и закрытия окна. 5.8.2. Прозрачные окна с содержимым необычной формы В большинстве случаев фиксированная графика в WPF для создания окон необычной формы не используется. Вместо этого в таких окнах просто применяется совершенно прозрачный фон, на котором затем размещается уже имеющее нужную форму содержимое. Преимущество такого подхода заключается в том, что он является модульным. Окно может состоять из множества отдельных компонентов, представляющих собой первоклассные элементы WPF. Но еще более важно то, что такой подход позволяет пользоваться другими функциональными возможностями WPF и создавать по-настоящему динамические пользовательские интерфейсы. Например, он позволяет создавать содержимое необычной формы с возможностью изменения его размеров или применять анимацию для обеспечения непрерывно выполняющихся эффектов прямо внутри окна. Создать 70 такое содержимое весьма не просто, если графика находится в одном статическом файле. Соответствующий пример показан на рис. 18. Здесь окно содержит элемент Grid с одной единственной ячейкой. Эту ячейку совместно используют два элемента. Первый из них — элемент Path, прорисовывающий границу окна нестандартной формы и заливающий ее градиентным узором, а второй — контейнер макета, в котором находится предназначенное для окна содержимое, перекрывающее элемент Path. В рассматриваемом случае в качестве контейнера макета служит StackPanel. Отметим, что с технической точки зрения это может быть и какой — либо другой контейнер (например, еще один элемент Grid или элемент Canvas для абсолютного позиционирования). В рассматриваемом случае в элементе StackPanel находятся кнопка закрытия и текст. Рис. 18. Окно нестандартной формы Ключевым компонентом в рассматриваемом примере является элемент Path, который создает фон. Он является простой векторной фигурой, состоящей из ряда линий и дуг. Ниже приведен код разметки, необходимый для создания данного элемента Path: <Path Stroke="DarkGray" StrokeThickness="1" SnapsToDevicePixels="True"> <Path.Fill> <LinearGradientBrush StartPoint="0.2,0" EndPoint="0.8,1" > <LinearGradientBrush.GradientStops> <GradientStop Color="White" Offset="0"></GradientStop> <GradientStop Color="White" Offset="0.45"></GradientStop> <GradientStop Color="LightBlue" Offset="0.9"></GradientStop> 71 <GradientStop Color="Gray" Offset="1"></GradientStop> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Path.Fill> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigure StartPoint="20,0" IsClosed="True"> <LineSegment Point="140,0"></LineSegment> <ArcSegment Point="160,20" Size="20,20" SweepDirection="Clockwise"></ArcSegment> <LineSegment Point="160,60"></LineSegment> <ArcSegment Point="140,80" Size="20,20" SweepDirection="Clockwise"></ArcSegment> <LineSegment Point="70,80"></LineSegment> <LineSegment Point="70,130"></LineSegment> <LineSegment Point="40,80"></LineSegment> <LineSegment Point="20,80"></LineSegment> <ArcSegment Point="0,60" Size="20,20" SweepDirection="Clockwise"></ArcSegment> <LineSegment Point="0,20"></LineSegment> <ArcSegment Point="20,0" Size="20,20" SweepDirection="Clockwise"></ArcSegment> </PathFigure> </PathGeometry.Figures> </PathGeometry> </Path.Data> <Path.RenderTransform> <ScaleTransform ScaleX="1.3" ScaleY="1.3"></ScaleTransform> </Path.RenderTransform> </Path> В данной разметке элемент Path имеет фиксированный размер (так же, как и окно), однако его размер можно сделать и изменяемым, поместив элемент в контейнер Viewbox. Еще одно направление улучшения этого примера — придание кнопке закрытия окна более убедительного 72 внешнего вида, например, с помощью векторного значка X, прорисовываемого на красной поверхности. 5.8.3. Перемещение окон нестандартной формы Одним из ограничений окон нестандартной формы является то, что в них отсутствует неклиентская область со строкой заголовка, позволяющая пользователю легко перетаскивать окно по рабочему столу. В Windows Forms это было весьма сложной задачей — приходилось либо обеспечивать реакцию на события мыши MouseDown, MouseUp и MouseMove и перемещать окно вручную при выполнении пользователем щелчка и перетаскивания. В WPF эта задача решается гораздо легче за счет того, что в любое время можно инициировать режим перетаскивания окна путем вызова метода Window.DragMove(). Для того чтобы позволить пользователю перетаскивать окно необычной формы, которое было показано в примере выше, необходимо просто добавить и обработать для окна или того элемента в этом окне, который впоследствии будет выполнять роль строки заголовка, событие MouseLeftButtonDown: <TextBlock Text="Title Bar" Margin="1" Padding="5" MouseLeftButtonDown="titleBar_MouseLeftButtonDown"></TextBlock> К обработчику события потребуется добавить только одну строку кода: private void titleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { this.DragMove(); } Теперь окно будет следовать за курсором мыши по экрану до тех пор, пока пользователь не отпустит кнопку мыши. 5.8.4. Изменение размеров окон нестандартной формы Изменение размеров окна нестандартной формы — задача не простая. Если форма окна хотя бы немного напоминает прямоугольник, 73 наиболее простым подходом будет добавление в правый нижний угол элемента захвата и изменения размера путем установки для свойства ResizeMode значения CanResizeWithGrip. Однако при размещении такого элемента предполагается, что окно имеет форму, напоминающую прямоугольную. Например, в случае создания окна с эффектом скругленных краев за счет использования объекта Border, как было показано ранее на рис. 17, такой прием может сработать. Элемент захвата и изменения размера появится в правом нижнем углу и, в зависимости от того, насколько скругленным был сделан этот угол, разместится в пределах поверхности окна, которому принадлежит. Но в случае создания окна более экзотической формы с применением, например, элемента Path, как было показано на рис. 18, такой подход не сработает, поскольку элемент захвата и изменения размера возникнет в пустой области рядом с окном. Если добавление элемента захвата и изменения размера не подходит для окна данной формы или требуется разрешить пользователю изменять размеры окна путем перетаскивания его краев, придется приложить немного дополнительных усилий. Технически имеется два основных подхода для решения этой задачи. Первый — использовать .NET-функцию вызова платформы (PInvoke) для отправки сообщения Win32, изменяющего размер окна, а второй — просто отслеживать позицию курсора мыши при перетаскивании пользователем окна в одну сторону и изменять размер вручную установкой свойства Width. Ниже рассматривается пример применения второго подхода. Вне зависимости от используемого подхода, вначале необходимо выполнить одно действие - придумать способ для определения момента наведения пользователем курсора мыши на край окна. В WPF это легче всего сделать, разместив вдоль каждого края окна некоторый специальный элемент. Быть видимым этому элементу вовсе необязательно — фактически он даже может быть полностью прозрачным и единственной его задачей является перехват событий мыши. Одним из лучших кандидатов на эту роль является элемент Rectangle, который представляет собой элемент, прорисовывающий форму. Идеальным для этой задачи будет 74 элемент Rectangle толщиной в 5 единиц. Ниже демонстрируется, как можно расположить элемент Rectangle, позволяющий изменять размер с правой стороны в окне со скругленными краями, которое было показано на рис. 17: <Grid> <Rectangle Grid.RowSpan="3" Cursor="SizeWE" Fill="Transparent" Width="5" VerticalAlignment="Stretch" HorizontalAlignment="Right" MouseLeftButtonDown="window_initiateWiden" MouseLeftButtonUp="window_endWiden" MouseMove="window_Widen"></Rectangle> </Grid> В данном случае элемент Rectangle размещается в верхней строке, но для свойства RowSpan получает значение 3. Благодаря этому он растягивается на все три строки и занимает всю правую сторону окна. В свойстве Cursor указывается тот курсор мыши, который должен появляться при наведении мыши на этот элемент. В данном случае это курсор изменения размера под названием «запад-восток», имеющий форму двухконечной стрелки, которая указывает вправо и влево. Обработчики событий элемента Rectangle переключают окно в режим изменения размера, когда пользователь щелкает на краю. Здесь необходимо захватить мышь для обеспечения уверенности в том, что события будут продолжать поступать даже в случае перемещения мыши за счет перетаскивания с поверхности прямоугольника в какую-нибудь сторону. Захват мыши снимается тогда же, когда пользователь отпускает левую кнопку мыши. bool isWiden = false; private void window_initiateWiden(object sender, System.Windows.Input. MouseEventArgs e) { isWiden = true; } private void window_endWiden(object sender, System.Windows.Input. MouseEventArgs e) 75 { isWiden = false; // Make sure capture is released. Rectangle rect = (Rectangle)sender; rect.ReleaseMouseCapture(); } private void window_Widen(object sender, System.Windows.Input. MouseEventArgs e) { Rectangle rect = (Rectangle)sender; if (isWiden) { rect.CaptureMouse(); double newWidth = e.GetPosition(this).X + 5; if (newWidth > 0) this.Width = newWidth; } } 6. КОМАНДЫ В [4] были продемонстрированы маршрутизируемые события, которые можно использовать для ответа на множество различных действий мыши и клавиатуры. Однако события являются компонентом довольно низкого уровня. В реальном приложении функциональные возможности делятся на задачи, имеющие более высокий уровень. Эти задачи могут инициироваться различными действиями и через различные элементы пользовательского интерфейса, включая главные меню, контекстные меню, клавиатурные комбинации и панели инструментов. WPF позволяет определять эти задачи, называемые командами, и подключать элементы управления к ним, избегая написания повторяющегося кода обработки событий. Еще более важно то, что функция команд может управлять состоянием пользовательского интерфейса путем автоматического отключения элементов управления при недоступности свя76 занных команд. Она также предоставляет специальное место хранения и локализации текстовых заголовков команд. В данном разделе речь пойдет о том, как использовать заготовленные классы команд в WPF, как связывать их с элементами управления и как разработчику определять собственные команды. Также будут рассмотрены ограничения модели команд, такие как отсутствие журнала хронологии команд и отсутствие поддержки для используемой на уровне приложения функции Undo, и показано, как создавать свои собственные аналоги. 6.1. Общие сведения о командах В хорошо спроектированном приложении Windows логика приложения находится не в обработчиках событий, а закодирована в имеющих более высокий уровень методах. Каждый из этих методов представляет одну решаемую приложением «задачу» (task), каждая из которых может полагаться на дополнительные библиотеки. Самым очевидным способом использования такого дизайна является добавление обработчиков событий везде, где они нужны, и применение каждого из них для вызова соответствующего метода приложения. В этом случае код окна превращается в своеобразную систему коммутации, которая реагирует на ввод и пересылает запросы внутрь приложения. Хотя такой дизайн является вполне разумным, он часто является неэффективным. Многие задачи приложения могут инициироваться по различным маршрутам, из-за чего часто все равно приходится писать несколько обработчиков событий, вызывающих один и тот же метод приложения. В этом нет особых проблем, пока не приходится учитывать состояние пользовательского интерфейса. Рассмотрим пример. Пусть имеется программа, в состав которой входит метод для печати открытого в программе документа по имени PrintDocument(). Данный метод может инициироваться четырьмя способами: через главное меню (путем выбора в меню File (Файл) команды Print (Печать)), через контекстное меню, с помощью клавиатурной комбинации 77 и с помощью соответствующей кнопки в панели инструментов. В определенных моментах функционирования приложения задача PrintDocument() должна быть недоступной (например, отсутствие документа). Это подразумевает отключение соответствующих команд в двух меню и кнопки в панели инструментов таким образом, чтобы на них нельзя было выполнять щелчок, а также игнорирование соответствующей клавиатурной комбинации. Написание кода, включающего и отключающего соответствующие элементы управления при различных условиях, является весьма непростой задачей. Следует также отметить, что допущение в нем ошибки может привести к тому, что различные блоки кода состояния будут перекрываться неправильно, оставляя элемент управления в активном состоянии даже тогда, когда он не должен быть доступен. С точки зрения последующего сопровождения программного продукта такой код может стать причиной серьезным проблем. Написание и отладка подобного кода является одним из наименее приятных аспектов разработки Windowsприложений. Удивительно, но в наборе инструментальных средств Windows Forms не было никаких функциональных возможностей, которые могли бы облегчать выполнение подобных операций. Разработчики могли создавать необходимую им инфраструктуру самостоятельно, но большинство из них предпочитало этого не делать. WPF заполняет этот пробел, предлагая новую командную модель, которая предоставляет две следующих важных возможности: делегирование событий подходящим командам; поддержание включенного состояния элемента управления в синхронизированном виде с помощью состояния соответствующей команды. Командная модель WPF является не настолько прямолинейной. Для подключения к модели маршрутизируемых событий ей требуется несколько отдельных компонентов, которые будут рассмотрены в данном разделе. Однако в концептуальном плане модель является достаточно 78 простой. Каждое действие, инициирующее печать, отображается на одну и ту же команду. Эта команда с помощью привязки соединяется в коде со всего лишь одним обработчиком событий. Система команд WPF является прекрасным средством упрощения дизайна приложения. Однако в ней имеется несколько серьезных недостатков. В частности, WPF не поддерживает: отслеживание команд (например, хронология ранее выполненных команд); «невыполнимые» команды; команды, которые имеют состояние и могут находиться в различных «режимах» (например, команда, которая может включаться и отключаться). 6.2. Модель команд WPF Модель команд WPF состоит из большого количества компонентов. Ключевыми в ней являются четыре компонента: команды. Команда представляет задачу приложения и следит за тем, когда она может быть выполнена. Однако кода, собственно выполняющего задачу приложения, команды не содержат; привязки команд. Каждая привязка (binding) подразумевает соединение команды с имеющей к ней отношение логикой приложения, отвечающей за обслуживание определенной области пользовательского интерфейса. Такой факторизованный дизайн очень важен, потому что одна и та же команда может использоваться в нескольких местах в приложении и иметь в каждом из них разное предназначение. Для обеспечения подобного поведения служат разные привязки одной и той же команды; источники команд. Источник команды инициирует команду. Например, и элемент управления MenuItem, и элемент управления Button могут служить источниками команд. Щелчок на них в таком случае будет приводить к выполнению привязанной команды; 79 целевые объекты команд. Целевой объект команды — это элемент, для которого предназначена данная команда, т. е. элемент, на котором она выполняется. Например, команда Paste может вставлять текст в элемент TextBox, а команда OpenFile— отображать документ в элементе DocumentViewer. В зависимости от природы команды целевой объект может быть важен или неважен. Ниже будет подробно рассмотрен первый компонент — команда WPF. 6.2.1. Интерфейс ICommand Центром модели команд WPF является интерфейс System.Windows.Input.ICommand, определяющий способ, в соответствии с которым работают команды. Этот интерфейс включает два метода и одно событие: public interface ICommand { void Execute(object parameter); bool CanExecute(object parameter); event EventHandler CanExecuteChanged; } В простой реализации метод Execute() должен содержать логику приложения, касающуюся задачи (например, печати документа). Однако, как будет показано ниже, WPF является более совершенной технологией. Она использует метод Execute() для запуска более сложного процесса, который, в конечном счете, заканчивается возбуждением события, обрабатываемого в совершенно другом месте в приложении. Это дает разработчику возможность использовать готовые классы команд и включать в них свою собственную логику, а также гибкость применения одной команды в нескольких различных местах. Метод CanExecute() возвращает информацию о состоянии команды — значение true, если она включена, и значение false, если она отключена. Методы Execute() и CanExecute() принимают дополнительный объектпараметр, который можно использовать для передачи с ними любой необ80 ходимой информации. Событие CanExecuteChanged вызывается при изменении состояния. Для любых использующих данную команду элементов управления оно является сигналом о том, что им следует вызвать метод CanExecute() и проверить состояние команды. Это часть связующего элемента, который позволяет источникам команд (например, элементу управления Button или элементу управления MenuItem) автоматически включать себя, когда команда доступна, и отключать, когда она не доступна. 6.3. Класс RoutedCommand При создании собственных команд реализовать интерфейс ICommand напрямую не обязательно. Вместо этого можно использовать класс System.Windows.Input.RoutedCommand, который автоматически реализует этот интерфейс. Класс RoutedCommand является единственным классом в WPF, который реализует интерфейс ICommand, поэтому все команды WPF представляют собой экземпляры класса RoutedCommand или производного от него класса. Одна из ключевых концепций, лежащих в основе модели команд в WPF, состоит в том, что класс RoutedCommand не содержит никакой логики приложения, он просто представляет команду. Это означает, что все объекты RoutedCommand обладает одинаковыми возможностями. Класс RoutedCommand добавляет дополнительную инфраструктуру для туннелирования и перемещения событий. Если интерфейс ICommand инкапсулирует идею команды — действие, которое может инициироваться и быть доступным или недоступным, то класс RoutedCommand изменяет команду таким образом, чтобы она могла подниматься вверх по иерархии элементов WPF до подходящего обработчика событий. Для поддержки маршрутизируемых событий класс RoutedCommand реализует интерфейс ICommand как закрытый и добавляет несколько отличающиеся версии его методов. Наиболее заметным изменением является то, что методы Execute() и CanExecute() теперь принимают дополнительный параметр. Новые сигнатуры этих методов выглядят следующим образом: 81 public void Execute(object parameter, IInputElement target) {...} public bool CanExecute(object parameter, IInputElement target) {...} Здесь target — это целевой элемент, в котором начинается обработка события. Это событие начинает обрабатываться в целевом элементе и затем поднимается вверх до находящихся на более высоком уровне контейнеров до тех пор, пока приложение не использует его для выполнения подходящей задачи. Помимо этого класс RoutedCommand также вводит три свойства: Name — имя команды; OwnerType — класс, членом которого является данная команда; коллекция InputGestures, представляющая любые клавиши, клавиатурные комбинации или действия с мышью, которые также могут применяться для вызова данной команды. 6.3.1. Перемещение событий Исходя из модели команд WPF, трудно понять точно, почему команды WPF требуют использования маршрутизируемых событий. Логичным было бы предположение, что объект команды должен заботиться о ее выполнении вне зависимости от того, как она вызывается. Это было бы так, если бы для создания собственных команд интерфейс ICommand нужно было использовать напрямую. Код нужно было бы определять внутри команды, так чтобы он работал бы одинаково независимо от того, что приводит к ее инициации. В перемещении событий не было бы никакой необходимости. Однако WPF использует ряд заготовленных команд. Классы этих команд не содержат никакого реального кода. Они являются просто удобно определенными объектами, которые представляют некоторую общую задачу приложения (например, печать документа). Для выполнения действий над этими командами необходимо использовать привязку (binding), вызывающую в коде соответствующее событие. Для обеспечения возможности выполнения данного события в одном месте, даже если 82 оно возбуждается разными источниками команд в одном и том же окне, как раз и необходима возможность перемещения событий. Преимущество заготовленных команд состоит в том, что они предлагают гораздо более удобные возможности для интеграции. В качестве примера предположим, что некий сторонний разработчик создал элемент управления DocumentView, использующий заготовленную команду Print. Если в разрабатываемом приложении применяется такая же заготовленная команда, разработчику не придется прилагать никаких дополнительных усилий для включения в него возможности печати. С этой точки зрения команды являются одним из главных компонентов архитектуры WPF. 6.4. Класс RoutedUICommand и библиотека команд Большинство команд, с которыми будет работать разработчик, будут не объектами RoutedCommand, а экземплярами класса RoutedUICommand, который наследуется от класса RoutedCommand. Класс RoutedUICommand предназначен для команд с текстом, который должен отображаться гделибо в пользовательском интерфейсе (например, текстом для элемента меню или текстом подсказки для кнопки в панели инструментов). Он добавляет единственное свойство — Text. В этом свойстве указывается текст, который будет отображаться для данной команды. Преимущество определения текста команды с командой, а не в элементе управления, заключается в том, что появляется возможность выполнять локализацию в одном месте. Разработчики WPF учли тот факт, что в каждом приложении может использоваться огромное количество команд, и что многие команды могут быть общими для множества приложений. Например, во всех приложениях, предназначенных для обработки документов, будут присутствовать версии команд New (Создать), Open (Открыть) и Save (Сохранить). Поэтому для уменьшения объема усилий, необходимых для создания таких команд, в состав WPF была включена библиотека базовых команд, в которой содержится более 100 команд. Все эти команды доступны через статические свойства пяти соответствующих статических классов: 83 ApplicationCommands. Этот класс предоставляет общие команды, включая команды, связанные с буфером обмена (Сору (Копировать), Cut (Вырезать) и Paste (Вставить)), и команды, касающиеся обработки документов (New (Создать), Open (Открыть), Save (Сохранить), Save As (Сохранить как), Print (Печать) и т. д.); NavigationCommands. Этот класс предоставляет команды, используемые для навигации, включая те, что предназначены для страничных приложений (BrowseBack (Назад), BrowseForward (Вперед) и NextPage (Переход)), и те, что подходят для приложений, предназначенных для работы с документами (команды IncreaseZoom (Масштаб) и Refresh (Обновить)); EditingCommands. Этот класс предоставляет длинный перечень команд, предназначенных по большей части для редактирования документов, включая команды для перемещения (MoveToLineEnd (Переход в конец строки), MoveLeftByWord (Переход влево на одно слово), MoveUpByPage (Переход на одну страницу вверх) и т. д.), выделения содержимого (SelectToLineEnd (Выделение до конца строки), SelectLeftByWord (Выделение слова слева)) и изменения форматирования (ToggleBold (Выделение полужирным) и ToggleUnderline (Выделение подчеркиванием)); MediaCommands. Этот класс включает набор команд для работы с мультимедиа (среди них команда Play (Воспроизвести), Pause (Пауза), NextTrack (Переход к следующей композиции) и IncreaseVolume (Увеличение громкости)). Класс ApplicationCommands предоставляет ряд основных команд, которые наиболее часто используются во всех типах приложений, поэтому с ними стоит ознакомиться. Полный перечень команд этого класса выглядит следующим образом: New (Создать); Open (Открыть); Save (Сохранить); 84 Save As (Сохранить как); Close (Закрыть); Print (Печать); Print Preview (Предварительный просмотр); CancelPrint (Отмена печати); Сору (Копировать); Cut (Вырезать); Paste (Вставить); Delete (Удалить); Undo (Отменить); Redo (Повторить); Find (Найти); Replace (Заменить); SelectAll (Выделить все); Stop (Остановить); ContextMenu (Контекстное меню); CorrectionList (Список исправлений); Properties (Свойства); Help (Справка). Например, команда ApplicationCommands.Open является статическим свойством, которое предоставляет объект RoutedUICommand. Этот объект представляет в приложении команду "Open" (Открыть). Поскольку ApplicationCommands.Open представляет собой статическое свойство, во всем приложении может существовать всего лишь один экземпляр команды Open. Однако применяться он может по-разному, в зависимости от его источника, т. е. того места, где он встречается в пользовательском интерфейсе. Свойство RoutedUICommand.Text отображает имя каждой команды, добавляя, где нужно, пробелы между словами. Например, для команды ApplicationCommands.SelectAll оно отображает текст «Select All» (Выделить все). Отметим, что свойство Name отображает тот же самый текст, но 85 без пробелов. Свойство RoutedUICommand.OwnerType возвращает тип объекта для класса ApplicationCommands, поскольку команда Open является статическим свойством этого класса. Как было сказано выше, все эти отдельные объекты команд являются всего лишь маркерами, не имеющими никакой реальной функциональности. Однако у многих из них имеется одна дополнительная функция: привязка ввода по умолчанию. Например, команда ApplicationCommands.Open отображается на комбинацию клавиш <Ctrl+О>. После привязки этой клавиатурной комбинации к команде и ее добавления в окно в виде источника данной команды она становится активной, даже если команда и не отображается нигде в пользовательском интерфейсе. 6.5. Выполнение команд Выше были рассмотрены базовые классы и интерфейсы команд, а также библиотека команд, которую WPF предлагает для использования. Однако не приводилось ни одного примера применения этих команд. Как показано выше, объект RoutedUICommand не имеет никаких жестко закодированных функциональных возможностей. Он просто представляет команду. Для инициализации этой команды необходимо использовать источник команды (или специальный код), а для ответа на нее — привязку команды с переадресацией ее выполнения обычному обработчику событий. 6.5.1. Источники команд Команды в библиотеке команд всегда доступны. Самый простой способ инициировать их — это привязать к элементу управления, реализующему интерфейс ICommandSource. К таковым относятся элементы управления, унаследованные от ButtonBase (Button, CheckBox и т. д.), а также отдельные объекты ListBoxItem, элемент Hyperlink и элемент MenuItem. 86 Интерфейс ICommandSource предоставляет три свойства, которые перечислены в таблице 7. Таблица 7 Свойства интерфейса ICommandSource Имя Command Описание Указывает на связанную команду CommandParameter Предоставляет данные, передаваемые с командой CommandTarget Определяет элемент, в котором должна выполняться данная команда Например, ниже показан код, в котором с помощью свойства Command кнопка связывается с командой ApplicationCommands.New: <Button Command="ApplicationCommands.New">New</Button> WPF является достаточно интеллектуальной для того, чтобы выполнять поиск по всем пяти описанным выше классам-контейнерам команд, а это значит, что предыдущую строку кода можно записать и короче: <Button Command="New">New</Button> Однако такой синтаксис может показаться менее явным и, следовательно, менее понятным, потому что он не указывает, в каком именно классе содержится команда. 6.6. Привязки команд При присоединении команды к источнику команды можно обнаружить, что источник команды будет автоматически отключен. Например, если создать показанную в предыдущем разделе кнопку New (Создать), она появится как затененная и недоступная для щелчка, как если бы для ее свойства IsEnabled было установлено значение false (рис. 19). Это происходит потому, что кнопка запросила состояние команды, а из-за отсутствия у команды привязки она считается отключенной. 87 Рис. 19. Кнопка с командой без привязки Чтобы изменить эту ситуацию, потребуется создать для команды привязку и указать три перечисленных ниже вещи: действие, которое должно выполняться при инициировании команды; способ определения того, может ли команда быть выполнена, т. е. доступна ли она. Данный параметр не является обязательным. Если его опустить, команда всегда будет являться доступной при наличии присоединенного обработчика событий; область, на которую должно распространяться действие команды. Например, она может ограничиваться одной единственной кнопкой, или распространяться на все окно (этот вариант используется чаще). Ниже показан фрагмент кода, в котором создается привязка для команды New. Этот код может быть добавлен к конструктору окна: CommandBinding binding; binding = new CommandBinding(ApplicationCommands.New); binding.Executed += NewCommand; this.CommandBindings.Add(binding); Важно отметить, что готовый объект CommandBinding добавляется в коллекцию содержащего окна, имеющую название CommandBindings. Работа этой конструкции осуществляется за счет перемещения событий. Фактически, при выполнении щелчка на кнопке событие CommandBinding.Executed «поднимается» от уровня кнопки до уровня содержащих элементов. 88 Хотя обычно все привязки добавляются в окно, свойство CommandBindings определено в базовом классе UIElement, поэтому оно поддерживается любым элементом. Например, приведенный пример работал бы точно также, даже если бы привязка команды была добавлена и прямо в код использующей эту команду кнопки, хотя использовать ее повторно с каким-то другим элементом более высокого уровня было бы невозможно. Для получения наибольшей гибкости, привязки команд обычно добавляются в окно наивысшего уровня. Если необходимо использовать ту же самую команду в более чем одном окне, привязку потребуется создавать в обоих окнах. В показанном выше коде предполагается, что в том же самом классе имеется готовый к получению команды обработчик событий по имени NewCommand. Ниже показан пример простого кода для отображения источника команды: private void NewCommand(object sender, ExecutedRoutedEventArgs e) { MessageBox.Show("New command triggered with " + e.Source.ToString()); } Теперь при запуске приложения кнопка является доступной (рис. 20). В случае выполнения на ней щелчка возбуждается событие Executed, которое затем поднимается до уровня окна и обрабатывается показанным ранее обработчиком NewCommand(). Рис. 20. Кнопка с привязкой команды На этом этапе WPF сообщает об источнике событие (кнопке). Объект ExecutedRoutedEventArgs также позволяет извлечь ссылку на команду, которая была вызвана (т. е. команду ExecutedRoutedEventArgs.Command), и любую дополнительную информацию, которая была передана вместе с ней (параметр ExecutedRoutedEventArgs.Parameter). В данном примере ни- 89 какая дополнительная информация не передавалась, поэтому значением параметра ExecutedRoutedEventArgs.Parameter будет null. В показанном выше примере привязка команды была сгенерирована с помощью кода. Однако команда так же легко может быть привязана и декларативным образом с помощью XAML, если требуется упростить лежащий в основе кода файл. Необходимый для этого код разметки выглядит следующим образом: <Window x:Class="WpfApplication2.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="161" Width="207"> <Window.CommandBindings> <CommandBinding Command="ApplicationCommands.New" Executed="NewCommand"> </CommandBinding> </Window.CommandBindings> <Grid Height="43" Width="105"> <Button Command="ApplicationCommands.New">New</Button> </Grid> </Window> Результат нажатия кнопки, определенной таким образом, показан на рис. 20. Отметим, что Visual Studio не предлагает никакой поддержки для определения привязок команд во время проектирования, а также предоставляет относительно слабую поддержку для подключения элементов управления и команд. Окно Properties (Свойства) позволяет устанавливать для элемента управления свойство Command (Команда), однако вводить точное имя команды нужно разработчику — раскрывающегося списка возможных вариантов команд для выбора не предусмотрено. 6.7. Использование множества источников команд Пример с кнопкой несколько напоминает обходной путь для инициации обычного события. Однако дополнительный уровень команды приобретает отчетливый смысл при добавлении большего количества исполь90 зующих эту команду элементов управления. Например, можно добавить элемент меню, также работающий с командой New: <Menu> <MenuItem Header="File"> <MenuItem Command="New"></MenuItem> </MenuItem> </Menu> Отметим, что данный объект MenuItem для команды New не устанавливает свойство Header. Это происходит потому, что элемент управления MenuItem способен извлекать текст из команды в случае, если свойство Header не устанавливается. Эта особенность играет очень важную роль, если планируется локализация приложения на разных языках. В таком случае изменить текст в одном месте легче, чем отслеживать его во всех окнах. У класса MenuItem есть еще одна функция. Он автоматически выбирает первую клавишу быстрого вызова команды, которая содержится в коллекции Command.InputBindings, если таковая имеется. В случае объекта ApplicationsCommands.New это означает, что в меню рядом с текстом появляется клавиатурная комбинация (Рис. 21). Рис. 21. Элемент меню, использующий команду Отметим, что создавать еще одну привязку команды для элемента меню не нужно. Одна привязка, созданная ранее, теперь применяется двумя разными элементами управления, оба из которых передают свою работу одному и тому же обработчику событий. 91 6.7.1. Точная настройка текста команды Способность меню автоматически извлекать текст элемента команды приводит к вопросу о том, а можно ли такое же делать с другими классами ICommandSource, например, с элементом управления Button. Можно, но для этого потребуются дополнительные усилия. В частности, для многократного использования текста команды существуют два способа. Первый подразумевает извлечение текста прямо из статического объекта команды. XAML позволяет выполнить подобное с помощью расширения Static. Ниже показан пример кода, который извлекает имя команды "New" (Создать) и использует его в качестве текста для кнопки: <Button Margin="5" Padding="5" Command="ApplicationCommands.New" ToolTip="{x:Static ApplicationCommands.New}">New</Button> Проблема этого варианта решения состоит в том, что он предполагает просто вызов на объекте команды метода ToString(), что позволяет получить имя команды, но не ее текст. В случае команд, состоящих из множества слов, лучше использовать текст, а не имя команды, поскольку текст включает пробелы. Поэтому предпочтительным решением считается применение выражения привязки данных. Эта привязка данных является немного необычной, поскольку подразумевает привязку к текущему элементу, захват используемого объекта Command и извлечение его свойства Text. Весь необходимый для этого код показан ниже: <Button Margin="5" Padding="5" Command="ApplicationCommands.New" Content = "{Binding RelativeSource={RelativeSource Self}, Path=Command.Text} "> </Button> 6.7.2. Прямой вызов команды При запуске команды применение классов, реализующих ICommandSource, не является единственным возможным вариантом. Команду можно также и просто вызывать напрямую из любого обработчика событий с помощью метода Execute(). В этом случае требуется всего лишь 92 передать значение параметра (или значение null) и сослаться на целевой элемент: ApplicationCommands.New.Execute(null, targetElement); Целевой элемент — это просто элемент, в котором WPF начинает искать привязку команды. В качестве такого элемента можно применять как содержащее окно (имеющее привязку команды), так и вложенный элемент окна. Отметим, что вызов метода Execute() также еще можно делать и в ассоциируемом объекте CommandBinding. В таком случае указывать целевой элемент не нужно, потому что в качестве него будет автоматически применяться тот элемент, который предоставляет используемую коллекцию CommandBindings: this.CommandBindings[0].Command.Execute(null); При таком подходе модель команд задействована лишь наполовину. Подход позволяет инициировать команду, но не предоставляет возможности для реагирования на изменение ее состояния. Если необходимо создать такую функцию, то необходимо дополнительно обработать событие RoutedCommand.CanExecuteChanged, чтобы обеспечить соответствующую реакцию на изменение состояния команды. При инициализации события CanExecuteChanged необходимо будет вызвать метод RoutedCommand. CanExecute() для проверки того, находятся ли команды в пригодном для использования состоянии, и если нет — отключить или изменить содержимое в соответствующей части пользовательского интерфейса. 6.7.3. Отключение команд Преимущества модели команд по-настоящему проявляются при создании команды, меняющей свое состояние с активного на неактивное и наоборот. Например, рассмотрим приложение с одним окном, показанное на рис. 22. Это простой текстовый редактор, состоящий из меню, панели инструментов и большого текстового поля (TextBox) и позволяющий открывать файлы, создавать новые (пустые) документы и сохранять свою работу. 93 Рис. 22. Простой текстовый редактор В данном случае вполне логично будет сделать команды New (Создать), Open (Открыть), Save (Сохранить), Save As (Сохранить как) и Close (Закрыть) доступными всегда. Однако в другом дизайне может потребоваться, чтобы команда Save становилась доступной только в том случае, если текст был изменен и стал отличаться от исходного текста. Условно эту деталь можно отследить в коде с помощью простого булевского значения и затем устанавливать этот флаг при каждом изменении текста: private bool isDirty = false; private void txt_TextChanged(object sender, RoutedEventArgs e) { isDirty = true; } Теперь необходимо только обеспечить возможность попадания данной информации из окна в привязку команды, так чтобы соответствующие элементы управления могли обновляться должным образом. Секрет заключается в обработке события CanExecute привязки команды. Присоединить обработчик к этому событию можно либо с помощью следующего кода: binding = new CommandBinding(ApplicationCommands.Save); binding.Executed += SaveCommand_Executed; binding.CanExecute += SaveCommand_CanExecute; 94 this.CommandBindings.Add(binding); либо декларативным образом: <Window.CommandBindings> <CommandBinding Command="ApplicationCommands.Save" Executed="SaveCommand_Executed" CanExecute="SaveCommand CanExecute"> </CommandBinding> </Window.CommandBindings> В этом обработчике событий нужно просто проверить значение переменной isDirty и установить соответствующее значение для свойства CanExecuteRoutedEventArg.CanExecute: private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = isDirty; } Если значением свойства isDirty оказывается false, то команда отключается (делается недоступной), а если true, то включается (делается доступной). Применяя CanExecute, важно помнить следующее: WPF самостоятельно решает, когда вызывать метод RoutedCommand.CanExecute() для запуска обработчика событий и определения состояния команды. Диспетчер команд WPF делает это только тогда, когда обнаруживает изменение, являющееся значительным с его точки зрения — например, при перемещении фокуса с одного элемента управления на другой или после выполнения команды. Элементы управления могут также инициировать событие CanExecuteChanged, указывающее WPF оценить состояние команды заново, например, подобное происходит при нажатии клавиши (например, при нажатии клавиши в текстовом поле). В целом событие CanExecute будет возбуждаться довольно часто, поэтому не следует использовать внутри него код с длительным временем выполнения. 95 Однако на состояние команды могут влиять и другие факторы. В данном примере флаг isDirty мог бы быть изменен в ответ на еще какоенибудь действие. Обнаружив, что состояние команды не обновляется в нужное время, можно вынудить WPF вызвать метод CanExecute() на всех используемых командах. Делается это вызовом статического метода CommandManager. InvalidateRequerySuggested(). После этого диспетчер команд запускает событие RequerySuggested, чтобы уведомить об этом все существующие в окне источники команд (кнопки, элементы меню и т. д.), которые затем повторно запрашивают информацию о состоянии связанных с ними команд и обновляют себя соответствующим образом. 6.8. Ограничения команд WPF Команды WPF способны изменять только один аспект состояния связанного с ними элемента, а именно — значение его свойства IsEnabled. Однако могут возникнуть ситуации, когда требуется более сложная логика. Например, может возникнуть необходимость создать команду PageLayoutView, которую можно было бы включать и отключать. При ее включении соответствующие элементы управления должны настраиваться соответствующим образом. Например, связанный элемент меню должен отмечаться флажком, а связанная кнопка в панели инструментов — выделяться, как выделяется элемент CheckBox при его добавлении в элемент ToolBar. К сожалению, возможности отслеживать состояние команды типа «установлен флажок» не существует. Это означает, что обрабатывать событие для этого элемента управления и обновлять его состояние и состояние других связанных элементов необходимо вручную. Простого решения для этой проблемы не существует. Даже если создать специальный класс, унаследованный от RoutedUICommand, и обеспечить его функцией для отслеживания состояния «отмечен/не отмечен» и вызова в случае его изменения соответствующего события, без замены части связанной инфраструктуры все равно не обойтись. Например, в данном случае пришлось бы создать специальный класс CommandBinding, который бы мог прослушивать уведомления от специальной команды, реагировать при из- 96 менении ее состояния «отмечен/не отмечен» и затем обновлять соответствующим образом все связанные с ней элементы управления. Кнопкифлажки являются явным примером выходящего за рамки модели команд состояния пользовательского интерфейса. Однако подобный дизайн возможен и в других случаях. Например, можно создать некоторую разделительную кнопку с возможностью переключения в разные «режимы». Распространить это изменение до других связанных элементов управления с помощью одной только модели команд опять-таки не получится. 6.9. Элементы управления со встроенными командами Некоторые из элементов управления, принимающих вводимые данные, обрабатывают события команд самостоятельно. Например, класс TextBox обрабатывает команды Cut, Сору и Paste (а также команды Undo и Redo и некоторые из команд класса EditingCommands, которые выделяют текст и перемещают курсор в разные позиции). Когда у элемента есть своя собственная, жестко закодированная командная логика, разработчику не нужно ничего делать, чтобы заставить команду работать. Например, если взять простой текстовый редактор, который был показан на рис. 22, и добавить в него кнопки для панели инструментов, то поддержка вырезания, копирования и вставки текста появится автоматически: <ToolBar> <Button Command="Cut">Cut</Button> <Button Command="Copy">Copy</Button> <Button Command="Paste">Paste</Button> </ToolBar> После добавления этой панели инструментов можно будет сразу же щелкать на любой из этих кнопок и выполнять соответствующие действия с текстом из буфера обмена. Следует отметить, что элемент управления TextBox также обрабатывает событие CanExecute. Если в нем в текущий момент ничего не будет выделено, команды Cut и Сору будут недоступны. А в случае перемещения фокуса на другой элемент управления, не поддерживающий команды Copy, Cut и Paste, сразу же автоматически будут 97 отключены все три команды, если к этому элементу не был специально присоединен включающий их обработчик событий CanExecute. В этом примере есть одна интересная деталь. Команды Copy, Cut и Paste обрабатываются элементом управления TextBox, на который наведен фокус. Однако каждая из них инициируется соответствующей кнопкой в панели инструментов, которая представляет собой уже совершенно отдельный элемент. В данном примере этот процесс проходит гладко, потому что кнопки размещены в панели инструментов, а класс ToolBar включает встроенную логику, которая для свойства CommandTarget его потомков динамически устанавливает в качестве значения именно тот элемент управления, на котором в текущий момент находится фокус. В случае размещения кнопок в другом контейнере (отличном от ToolBar и Menu) такого преимущества не будет. То есть кнопки не будут работать до тех пор, пока для них вручную не будет установлено свойство CommandTarget, для чего придется использовать выражения привязки с именем целевого элемента. Например, если текстовое поле имеет имя txtDocument. кнопки потребуется определить следующим образом: <Button Command="Cut" CommandTarget="{Binding ElementName=txtDocument}">Cut</Button> <Button Command="Copy" CommandTarget="{Binding ElementName=txtDocument}">Copy</Button> <Button Command="Paste" CommandTarget="{Binding ElementName=txtDocument}">Paste</Button> Другой вариант состоит в создании новой области действия фокуса с помощью присоединяемого свойства FocusManager.IsFocusScope. Это укажет WPF, что при срабатывании команды следует искать элемент в родительской области действия фокуса: <StackPanel FocusManager.IsFocusScope="True"> <Button Command="Cut">Cut</Button> <Button Command="Copy">Copy</Button> <Button Command="Paste">Paste</Button> </StackPanel> 98 Такой подход обладает дополнительным преимуществом, позволяя применять одни и те же команды к множеству элементов управления в отличие от предыдущего примера, где свойство CommandTarget кодировалось жестким образом. 6.10. Усовершенствованные команды Выше были рассмотрены основные характеристики команд. В этом разделе будет показано, как разработчик может использовать собственные команды, как обеспечить различие их восприятия в зависимости от целевого элемента, и как применять параметры команд, а также о том, как обеспечить поддержку для базовой функции отмены (Undo). 6.10.1. Специальные команды Какими бы полными не были пять стандартных классов команд (ApplicationCommands, NavigationCommands, EditingCommands, ComponentCommands и MediaCommands), очевидно, что они не покрывают все команды, которые возможно потребуются для реализации приложения. WPF позволяет разработчику сравнительно легко определять свои собственные специальные команды. Все, что для этого требуется сделать — это создать новый экземпляр объекта RoutedUICommand. Класс RoutedUICommand имеет несколько конструкторов. Его можно создавать и безо всякой дополнительной информации, но в большинстве случаев будет требоваться предоставить имя команды, текст команды и тип владения. Вдобавок также может возникнуть желание предоставить и сокращенную клавиатурную команду для коллекции InputGestures. Наилучший подход — последовать примеру библиотек WPF и предоставлять свои специальные команды через статические свойства. Ниже показан пример с командой под названием Requery (Повторный запрос): public class DataCommands { private static RoutedUICommand requery; static DataCommands() 99 { // Инициализация команды. InputGestureCollection inputs = new InputGestureCollection(); inputs.Add(new KeyGesture(Key.R, ModifierKeys.Control, "Ctrl+R")); DataCommands.requery = new RoutedUICommand("Requery", "Requery", typeof(DataCommands), inputs); } public static RoutedUICommand Requery { get { return DataCommands.requery; } } } Определив команду, ее можно использовать в привязках команд точно так же, как и любую из готовых команд, которые предлагает WPF. Однако присутствует одна особенность: если нужно использовать команду в XAML, сначала потребуется отобразить разрабатываемое пространство имен .NET на пространство имен XML. Например, если класс находится в пространстве имен под названием Commands, необходимо добавить следующую строку: xmlns:local="clr-namespace:Commands" В данном примере в качестве псевдонима для пространства имен было выбрано слово local. Однако разрешено использовать любой псевдоним, главное придерживаться единообразного стиля XAML-файла. Теперь к команде можно получать доступ через пространство имен local: <CommandBinding Command="local:DataCommands.Requery" Executed="RequeryCommand"/> Ниже показан весь код примера простого окна с кнопкой, запускающей команду Requery: <Window x:Class="Commands.CustomCommand" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Commands" Height="300" Width="300" 100 xmlns:local="clr-namespace:Commands" > <Window.CommandBindings> <CommandBinding Command="local:DataCommands.Requery" Executed="RequeryCommand"/> </Window.CommandBindings> <Grid> <Button Margin="5" Command="local:DataCommands.Requery">Requery</Button> </Grid> </Window> Для полноты примера необходимо реализовать в программном коде обработчик событий RequeryCommand_Executed(), а также событие CanExecute, чтобы иметь возможность по выбору включать или отключать команду. 6.11. Использование одной команды в разных местах Одной из ключевых идей в модели команд WPF является область действия. Хотя фактически существует только одна копия каждой команды, эффект применения команды варьируется в зависимости от места ее инициации. Например, в окне может быть два текстовых поля, причем оба они поддерживают команды Cut, Сору и Paste, а соответствующая операция будет выполняться только в том из них, на котором в текущий момент находится фокус. В качестве примера рассмотрим окно с текстовым редактором для двух документов, как показано на рис. 23. 101 Рис. 23. Текстовый редактор для двух документов Команды Cut, Сору и Paste будут работать автоматически, а реализованные самостоятельно команды New, Open и Save — нет. Сложность состоит в том, что при срабатывании события Executed для любой из этих команд совершенно не очевидно, к какому из текстовых полей оно относится. Хотя объект ExecutedRoutedEventArgs и предоставляет свойство Source, это свойство отражает элемент, имеющий привязку команды. А все привязки команд в текущий момент присоединены к содержащему окну. Решить эту проблему можно, привязав команду в каждом текстовом поле по-разному с помощью коллекции CommandBindings следующим образом: <TextBox.CommandBindings> <CommandBinding Command="ApplicationCommands.Save" ed="SaveCommand" /> </TextBox.CommandBindings> Execut- Теперь текстовое поле обрабатывает событие Executed. Эту информацию можно использовать в обработчике событий для обеспечения уверенности в том, что сохраняется правильная информация: private void SaveCommand(object sender, ExecutedRoutedEventArgs e) { string text = ((TextBox)sender).Text; MessageBox.Show("About to save: " + text); isDirty = false; 102 } Такая реализация имеет два недостатка. Первый состоит в том, что простой флаг isDirty больше не подходит, поскольку следить требуется за двумя текстовыми полями. Эту проблему можно решить двумя способами: воспользоваться для хранения флага isDirty свойством TextBox.Tag — в таком случае при каждом вызове метода CanExecuteSave() придется просто просматривать свойство Tag отправителя; создать для хранения значения isDirty закрытый словарь с индексацией по ссылке элемента управления — в таком случае при вызове метода CanExecuteSave() нужно будет просто отыскивать значение isDirty, принадлежащее отправителю. Ниже показан код, необходимый для реализации второго варианта: private Dictionary<Object, bool> isDirty = new Dictionary<Object, bool>(); private void txt_TextChanged(object sender, RoutedEventArgs e) { isDirty[sender] = true; } private void SaveCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) { if (isDirty.ContainsKey(sender) && isDirty[sender] == true) { e.CanExecute = true; } else { e.CanExecute = false; } } Другой недостаток текущей реализации связан с тем, что она подразумевает создание двух привязок команд, в то время как на самом деле необходима только одна. Это привносит беспорядок в файл XAML и 103 делает его более сложным в сопровождении. Данная проблема особенно значима при наличии большого количества команд, используемых совместно обоими текстовыми полями. Решить ее можно, создав одну единственную привязку команды и добавив ее в коллекцию CommandBindings обоих элементов TextBox. Подобное легко сделать в коде. При необходимости сделать это в XAML придется применить другую функциональную возможность, которая не рассматривается в данном учебном пособии, а именно — ресурсы WPF. Использование этой возможности означает, что нужно будет добавить раздел в верхней части окна, которое создает необходимый для использования объект, и присвоить ему имя ключа: <Window.Resources> <CommandBinding x:Key="binding" Command="ApplicationCommands.Save" Executed="SaveCommand" CanExecute="SaveCommand_CanExecute" /> </Window.Resources> Далее этот объект потребуется сохранить в словарной коллекции для того, чтобы к нему можно было получать доступ из любого места. Вставить объект в другое место в коде разметки можно будет при помощи расширения StaticResource и предоставления имени ключа: <TextBox.CommandBindings> <StaticResource ResourceKey="binding"></StaticResource> </TextBox.CommandBindings> 6.12. Использование параметра команды В примерах, рассмотренных выше, не встречалось использование параметра команды для передачи дополнительной информации. Однако некоторые команды всегда требуют предоставления какой-то дополнительной информации. Например, команде NavigationCommands.Zoom для изменения масштаба обязательно необходимо процентное значение. Исходя из этого, логично предположить, что в определенных сценариях дополнительная информация может быть нужна и уже использующимся командам. Например, команде Save в текстовом редакторе, 104 поддерживающем обработку сразу двух файлов (рис. 23), необходимо знать, к какому именно файлу должно применяться сохранение. Решение этой задачи заключается в установке свойства CommandParameter. Это можно сделать прямо в элементе управления ICommandSource, причем можно даже использовать выражение привязки, извлекающее значение из другого элемента управления. Например, ниже показано, как установить процент масштаба для кнопки, связанной с командой Zoom, за счет считывания значения из другого текстового поля. <Button Command="NavigationCommands.Zoom" CommandParameter="{Binding ElementName=txtZoom, Path=Text}"> Zoom To Value </Button> К сожалению, такой подход работает не всегда. Например, в текстовом редакторе, поддерживающем обработку сразу двух файлов, кнопка Save (Сохранить) просто повторно используется для каждого элемента TextBox, но каждому элементу TextBox необходимо указывать разные имена файлов. В подобных ситуациях следует либо сохранять информацию в каком-то другом месте (например, в отдельной коллекции, индексирующей имена файлов в соответствии с текстовыми полями), либо запускать команду программно следующим образом: ApplicationCommands.New.Execute(theFileName, (Button)sender); И в том и в другом случае параметр делается доступным в обработчике событий Executed через свойство ExecutedRoutedEventArgs.Parameter. 6.13. Отслеживание и отмена команд Один из недостатков модели Command — это отсутствие возможности делать команду обратимой. Хотя доступна команда ApplicationCommands. Undo, она обычно используется элементами редактирования, которые поддерживают свои собственные журналы данных для отката. Если планируется поддерживать функцию Undo в масштабах приложения, необходимо отслеживать предыдущее состояние внутренне и восстанавливать его при инициировании команды Undo. 105 Одним из возможных подходов будет разработка собственной системы для отслеживания и отмены команд и использование класса CommandManager для ведения хронологии этих команд. На рис. 24 показан пример применения именно такого подхода. Окно состоит из двух текстовых полей (TextBox), в который свободно можно вводить любой текст, и поля списка (ListBox), в котором отслеживается каждая команда, имевшая место в обоих текстовых полях. Отменить последнюю команду можно, щелкнув на кнопке Reverse Last Action (Отменить последнее действие). Рис. 24. Функция Undo в масштабе приложения Для создания такого решения требуется несколько новых деталей, первой из которых является класс для отслеживания хронологии команд. Некоторые разработчики могут попытаться создать специальную систему, которая будет хранить список недавно выполненных команд. К сожалению, такая система работать не будет, потому что все команды WPF воспринимаются как единичные экземпляры. Это означает, что в приложении существует только один экземпляр каждой команды. Чтобы понять, в чем состоит проблема, представим, что в приложении поддерживается команда EditingCommands.Backspace, и пользователь выполняет несколько таких команд подряд. Можно зарегистрировать этот факт путем добавления команды Backspace в стек недавних команд, но на 106 самом деле в стек несколько раз будет добавлен один и тот же объект. В результате получается, что простого способа сохранения с этой командой другой информации, например, символа, который был только что удален с ее помощью, не существует. При желании сохранить это состояние придется создать свою собственную структуру данных, которая будет выполнять эту задачу. В рассматирваемом примере для этого используется класс CommandHistoryItem. Каждый объект CommandHistoryItem отслеживает информацию о перечисленных ниже вещах: имя команды; элемент, для которого была выполнена эта команда. В данном примере имеется два текстовых поля, так что таким элементом может быть одно из них; свойство, которое было изменено в целевом элементе. В данном примере это будет свойство Text класса TextBox; объект, который можно использовать для сохранения предыдущего состояния задействованного элемента. Класс CommandHistoryItem также включает один метод — универсальный метод Undo(). Этот метод использует рефлексию для применения к измененному свойству предыдущего значения. Это работает в случае восстановления текста в элементе TextBox, но в более сложном приложении может потребоваться иерархия классов CommandHistoryItem, способных отменять действия разных типов разным образом. Ниже приведен полный код для класса CommandHistoryItem, который использует поддерживаемую в языке С# функцию автоматических свойств: public class CommandHistoryItem { public string CommandName {get; set;} public UIElement ElementActedOn {get; set;} 107 public string PropertyActedOn {get; set;} public object PreviousState {get; set;} public CommandHistoryItem(string commandName) : this(commandName, null, «», null) {} public CommandHistoryItem(string commandName, UIElement elementActedOn, string propertyActedOn, object previousState) { CommandName = commandName; ElementActedOn = elementActedOn; PropertyActedOn = propertyActedOn; PreviousState = previousState; } public bool CanUndo { get { return (ElementActedOn != null && PropertyActedOn != «»); } } public void Undo() { Type elementType = ElementActedOn.GetType(); PropertyInfo property = elementType.GetProperty(PropertyActedOn); property.SetValue(ElementActedOn, PreviousState, null); } } Следующим необходимым компонентом является команда, которая будет выполнять действие Undo в масштабах приложения. Как уже было сказано выше, команда ApplicationCommands.Undo для этого не подходит, поскольку она уже используется для отдельных элементов управления с другой целью. Поэтому нужно создать новую команду, как показано ниже: private static RoutedUICommand applicationUndo; public static RoutedUICommand ApplicationUndo 108 { get { return MonitorCommands.applicationUndo; } } static MonitorCommands() { applicationUndo = new RoutedUICommand( «ApplicationUndo», «Application Undo», typeof(MonitorCommands)); } В данном примере эта команда определяется в классе окна по имени MonitorCommands. Пока что код не производит впечатления сложного (за исключением небольшого фрагмента с кодом рефлексии, который выполняет операцию отмены). Самой сложной частью является интеграция в модель команд WPF журнала команд. Идеальным решением будет организовать это так, чтобы отслеживать можно было любую команду, независимо от того, как она инициируется и связывается. Обеспечить реагирование на конкретную команду довольно легко, однако задача состоит в обеспечении реагирования на выполнение любой команды. Решение заключается в использовании класса CommandManager, который предоставляет несколько статических событий. К числу этих событий относятся CanExecute, PreviewCanExecute, Executed и PreviewCanExecuted. В данном примере наиболее интересными являются два последних события, потому что они возбуждаются всякий раз, когда выполняется какая - либо команда. Событие Executed подавляется классом CommandManager, но все равно можно присоединить обработчик событий с помощью метода UIElement.AddHandler() и передать в нем в качестве необязательно третьего параметра значение true. Это позволит извлекать событие, даже несмотря на то, что оно обрабатывается. Однако событие Executed возбуждается после выполнения события, когда уже поздно сохранять информацию о состоянии задействованного элемента управления в журнале команд. Поэтому нужно обеспечивать реагирование на это событие, а не на 109 событие PreviewExecuted, которое возбуждается непосредственно перед этим. Ниже показан код, который присоединяет обработчик событий PreviewEvent в конструкторе окна и удаляет его при закрытии окна: public MonitorCommands() { InitializeComponent(); this.AddHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted)); } private void window_Unloaded(object sender, RoutedEventArgs e) { this.RemoveHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(CommandExecuted)); } При возбуждении события PreviewExecuted может потребоваться определение, стоит ли на данную команду обращать внимание, и если да, то должно выполниться создание элемента CommandHistoryItem и добавление его в стек Undo. Также необходимо позаботится об исключении вероятности возникновения двух потенциальных проблем. Во-первых, при щелчке на кнопке в панели инструментов для выполнения команды в текстовом поле событие CommandExecuted инициируется дважды: один раз для кнопки в панели инструментов, а второй — для текстового поля. В приведенном коде дублирование записей в журнале Undo исключается путем игнорирования команды в случае, если отправителем является ICommandSource. Во-вторых, команды, которые не следует добавлять в журнал Undo, должны игнорироваться явным образом. К числу таких команд относится команда ApplicationUndo, которая позволяет отменять предыдущее действие. Программный код, реализующий все вышесказанное, представлен ниже: private void CommandExecuted(object sender, ExecutedRoutedEventArgs e) { // Ignore menu button source. 110 if (e.Source is ICommandSource) return; // Ignore the ApplicationUndo command. if (e.Command == MonitorCommands.ApplicationUndo) return; // Could filter for commands you want to add to the stack // (for example, not selection events). TextBox txt = e.Source as TextBox; if (txt != null) { RoutedCommand cmd = (RoutedCommand)e.Command; CommandHistoryItem historyItem = new CommandHistoryItem( cmd.Name, txt, "Text", txt.Text); ListBoxItem item = new ListBoxItem(); item.Content = historyItem; lstHistory.Items.Add(historyItem); // CommandManager.InvalidateRequerySuggested(); } } В данном примере все объекты CommandHistoryItem сохраняются в элементе ListBox. Для свойства DisplayMember этого элемента устанавливается значение Name так, чтобы в нем отображалось свойство CommandHistoryltem.Name каждого объекта. В показанном коде функция Undo поддерживается только в том случае, если команда инициируется для текстового поля. Однако этот код является достаточно универсальным для того, чтобы работать с любым текстовым полем в окне, следовательно, его можно легко расширить так, чтобы он поддерживал и другие элементы управления и свойства. Последняя необходимая деталь — это код, выполняющий операцию отмены (Undo) в масштабах приложения. С помощью обработчика CanExecute можно сделать так, чтобы он выполнялся только при наличии в журнале Undo хотя бы одного элемента: private void ApplicationUndoCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) 111 { if (lstHistory == null || lstHistory.Items.Count == 0) e.CanExecute = false; else e.CanExecute = true; } Для отмены последнего изменения нужно просто вызвать для соответствующего объекта CommandHistoryItem метод Undo() и затем удалить его из списка: private void ApplicationUndoCommand_Executed(object sender, RoutedEventArgs e) { CommandHistoryItem historyItem = (CommandHistoryItem)lstHistory.Items[lstHistory.Items.Count - 1]; if (historyItem.CanUndo) historyItem.Undo(); lstHistory.Items.Remove(historyItem); } Несмотря на то, что приведенный пример демонстрирует концепцию и представляет простое приложение с множеством элементов управления, которые полностью поддерживают функцию Undo, прежде чем применять подобный подход в реальном приложении, потребуется внести еще немало корректировок. Например, придется создать детализацию обработчика событий CommandManager.PreviewExecuted таким образом, чтобы он игнорировал команды, которые точно не должны отслеживаться (например, события выделения текста с помощью клавиатуры или нажатия клавиши пробела) подобным образом, также может понадобиться добавить объекты CommandHistoryltem для действий, которые должны быть обратимыми, но не представлены командами, таких как ввод фрагмента текста и переход к другому элементу управления. И, наконец, возможно, еще возникнет потребность ограничить журнал Undo только самыми последними командами. 112 ЗАКЛЮЧЕНИЕ Технология Windows Presentation Foundation является на сегодняшний день самой перспективной технологией создания пользовательских интерфейсов программных продуктов. Основным преимуществом технологии является возможность разделения труда между разработчиком интерфейсов и разработчиком программной логики. Очевидно, что подобное разделение позволяет ускорить разработку программного обеспечения и повысить его качество. Использование классических элементов управления в WPF не исчерпывается рассмотренными в рамках пособия свойствами окон и элементов управления, а также механизмом команд. За рамками пособия остались ресурсы и стили для оформления элементов пользовательского интерфейса, механизмы привязки данных к интерфейсным элементам и шаблоны, позволяющие разработчику переопределить внешний вид всех элементов управления, включая стандартные, причем при определении внешнего вида могут учитываться привязанные данные. Подробнее о механизмах, не вошедших в данное учебное пособие, можно узнать в [1] и [2], а также по следующим адресам1: http://msdn.microsoft.com/ru-ru/library/ms754130.aspx; http://msdn.microsoft.com/ru-ru/netframework/aa663326; http://www.techdays.ru/Category.aspx?Tag=WPF. 1 Приведенные адреса актуальны на 18.03.2012 г. 113 ПРИЛОЖЕНИЕ А. ПРИМЕРЫ ТЕСТОВЫХ ЗАДАНИЙ2 1. Все элементы управления являются наследниками класса: o System.Windows.Control o System.Windows.Grid o System.User.Control o System.Windows.UserControl 2. Цвет фона и переднего плана элемента управления задается свойствами: o Background и Foreground o Back и Forward o First и Second o Color1 и Color2 3. Свойства Background и Foreground используют для отображения элемента: o объект цвета o объект Brush o цвет непосредственно o не используют дополнительных объектов 4. Для определения цвета по его составляющим используется метод: o Color.FromRgb o Color.ToRgb o Color. Rgb o Color.BGR 2 Вопросы, предполагающие один вариант ответа, обозначены знаком , предполагающие несколько вариантов ответа – знаком 114 5. Полностью прозрачному цвету и полностью непрозрачному цвету соответствуют следующие значения альфа-канала: o 0 и 255 соответственно o 255 и 0 соответственно o прозрачные цвета не допускаются 6. Сделать элемент прозрачным можно следующими способами: с помощью свойства Opacity с помощью полупрозрачного цвета с помощью специального дескриптора прозрачные элементы управления не допускаются 7. Наклонение текста, определяется объектом: o FontSize o FontStyle o FontWeight o FontStretch 8. Величина, на которую растягивается или сжимается текст, представлена объектом: o FontSize o FontStyle o FontWeight o FontStretch 9. Доступ к специализированным вариантам гарнитур шрифта осуществляется через свойство: o Typography o TextDecorations o FontFamily o TextWeight 115 10. Процесс передачи параметров шрифта всем вложенным элементам управления называется: o наследованием шрифта o маршрутизацией шрифта o распространением шрифта o преобразованием шрифта 11. Список шрифтов, установленных на компьютере можно получить при помощи коллекции: o System.FontFamilies o System.Fonts o System.FontStyles o подобная коллекция не определена 12. Указатель мыши для любого элемента управления задается свойством: o Cursor o Mouse o Pointer o MouseCursor 13. Каждый указатель мыши представлен объектом: o System.Windows.Input.Cursor o System.Windows.Input.Mouse o System.Windows.Input.Pointer o System.Windows.Input.MouseCursor 14. Стандартные указатели мыши определяются статическими свойствами класса: o Cursors o Mouses o Pointers o MouseCursors 116 15. Если необходимо применить параметры указателя к каждому элементу в каждом окне приложения, то необходимо использовать статическое свойство: o Mouse.OverrideCursor o Mouse.ReplaceCursor o Mouse.Cursor o Mouse.OverridedCursor 16. Отличительной чертой элемента Label является: o поддержка мнемонических команд o манипулирование одиночной порцией содержимого o отображение на форме o размещение при помощи менеджера композиции 17. WPF распознает следующие виды кнопок: Button CheckBox RadioButton GroupButton SpinButton 18. Все кнопки WPF являются наследниками класса: o ButtonBase o GroupBase o RadioBase 19. Укажите классы, являющиеся потомками класса ButtonBase: GridViewColumnHeader RepeatButton ToggleButton ScrollButton 20. Укажите классы, являющиеся потомками класса ToggleButton: CheckBox RadioButton RepeatButton ToggleButton 117 21. Группировка элементов управления RadioButton осуществляется при помощи свойства: o GroupName o GroupID o Group o ID 22. Свойство, определяющее положение контекстного указателя относительно другого элемента, называется: o PlacementTarget o PlacementRectangle o HorizontalOffset o VerticalOffset 23. Свойство, позволяющее поместить контекстное окно указателя со смещением, называется: o PlacementTarget o PlacementRectangle o HorizontalOffset o VerticalOffset 24. Настройка некоторых свойств класса ToolTip осуществляется при помощи класса: o ToolTipService o ToolTipManager o Grid o ToolTipBase 25. Содержимое элемента управления Popup определятся свойством: o Popup.Child o Popup.Content o Popup.Text o Такое свойство не определено 118 WPF включает следующие текстовые элементы управления: TextBox RichTextBox PasswordBox EditTextBox 27. Отличие элемента управления PasswordBox от TextBox заключается в следующем: хранение текста в зашифрованной форме отображение символов маски вместо текста возможность работы с многострочным текстом хранение вводимого текста в свойстве Text 28. Длина выделенного в TextBox текста определяется свойством: o SelectionStart o SelectionLength o SelectedText 29. Функция проверки орфографии в WPF: o не зависит от установленного программного обеспечения o зависит от установленного программного обеспечения o требует особого программного обеспечения o требует особого аппаратного обеспечения 30. Базовый класс для всех элементов управления, представляющих списки, является: o ItemsControl o ListControl o ItemKeeper o ListBase 31. Элементами управления WPF, использующими концепцию диапазонов, являются: ScrollBar ProgressBar Slider SpinButton 26. 119 32. Отличие элемента ProgressBar от элемента Slider заключается в том, что: значение свойства Value определяется исключительно программно присутствует возможность отображения хода процесса неопределенной длительности отсутствуют свойства Minimum и Maximum отсутствует свойство Value 33. Для отображения окна необходимо вызвать метод: Show() ShowModal() ShowWindow() выполнить присваивание Visible=true 34. Модальное окно отличается от немодального окна тем, что: o блокирует работу приложения до своего закрытия o не блокирует работу приложения до своего закрытия o блокирует работу операционной системы o отличий нет 35. Чаще всего используется стандартное диалоговое окно: o System.Windows.MessageBox o System.Windows.OpenDialog o System.Windows.SaveDialog o System.Windows.PrintDialog 36. Определите последовательность шагов (1, 2, 3), необходимую для создания окна нестандартной формы: o установить для свойства Window.AllowsTransparency значение true o установить для свойства Window.WindowStyle значение None o установить для фона окна прозрачный цвет или задать в качестве фонового изображения некоторое изображение, имеющее прозрачные области 120 37. Командная модель WPF представляет разработчику следующие возможности: делегирование событий подходящим командам поддержание включенного состояния элемента управления в синхронизированном виде с помощью состояния единое место хранение текстовой информации для локализации универсальные интерфейсы обработки данных 38. Укажите интерфейс, определяющий способ, в соответствии с которым работают команды: o System.Windows.Input.ICommand o System.Windows.Input.IRoutedCommand o System.Windows.Input.IUserCommand o System.Windows.Input.ICommonCommand 39. Все команды WPF являются потомками класса: o RoutedCommand o Command o UserCommand o CommonCommand 40. Определите последовательность шагов (1, 2, 3), выполняемую при создании привязки команды: o определение действия, которое должно выполняться при инициировании команды o указание способа определения того, может ли команда быть выполнена. o определение области, на которую должно распространяться действие команды. 121 41. Существуют ли элементы управления, имеющие встроенные команды? o Да, существуют o Да, существуют при определенных условиях o Нет, не существуют o Нет, не существуют при определенных условиях 42. Для передачи параметра команды используется свойство: o CommandParameter o CommandTarget o CommandSource o CommandValue 122 ПРИЛОЖЕНИЕ Б. ГЛОССАРИЙ System.Windows.Control – базовый класс, наследниками которого являются все элементы управления. Background, Foreground – свойства элемента управления, задающие цвет фона и переднего плана элемента соответственно. System.Windows.SystemColors – перечисление системных цветов, учитывающее цветовую схему операционной системы и цветовые предпочтения пользователя. Opacity – свойство элемента управления, определяющее его прозрачность. Принимает дробное значение в диапазоне 0..1, где 1 соответствует полностью непрозрачному цвету, а 0 — полностью прозрачному. TextDecorations – набор свойств шрифта, позволяющих добавить в текст некоторую разновидность линии. Они включают Baseline, OverLine, Strikethrough и Underline. Typography – свойство шрифта, позволяющее получать доступ к специализированным вариантам гарнитур, которые могут предоставить лишь некоторые шрифты. System.Windows.Input.Cursor – объект, представляющий курсор мыши. Label – простейший элемент управления содержимым, принимает одиночную порцию содержимого, которую нужно поместить внутри нее. Отличительной чертой элемента Label является его поддержка мнемонических команд — клавиш быстрого доступа, которые передают фокус связанному элементу управления. Button – класс, представляет обыкновенную кнопку Windows. ToggleButton – класс, представляет кнопку, имеющую два состояния (нажата и отпущена). ToggleButton – класс, наследниками которого являются CheckBox и RadioButton. Они предоставляют пользователю возможность включать и выключать их. В случае CheckBox включение элемента управления означает отметку в нем флажка. 123 TextBase – класс, наследниками которого являются элементы управления TextBox и RichTextBox. SelectionStart – свойство класса TextBox, определяющее позицию, начиная с нуля, в которой будет осуществляться выделение текста. SelectionLength – свойство класса TextBox, задающее общее количество выделенных символов. PasswordBox – элемент управления, выглядящий подобно элементу управления TextBox, однако отображающий строку, содержащую символы-кружочки, скрывающие собой настоящие символы. ListBox – класс, предоставляющий список переменной длины, который дает пользователю возможность выбирать элемент. ComboBox – элемент управления, хранящий коллекцию объектов ComboBoxItem, которые создаются явным или неявным образом, и использует раскрывающийся список, из которого пользователь может выбрать только один элемент за один раз. RangeBase – класс, наследниками которого являются три элемента управления, использующих концепцию диапазонов. Они принимают числовое значение, которое находится в диапазоне между заданными минимальным и максимальным значениями. ScrollBar, ProgressBar и Slider – элементы управления, основанные на диапазонах значений. ProgressBar – элемент управления, показывающий ход выполнения длительной задачи. Window – класс, представляющий окно приложения. System.Windows.MessageBox – класс, представляющий стандартное диалоговое окно общего назначения. Команда – задача высокого уровня, которая может инициироваться различными действиями и через различные элементы пользовательского интерфейса, включая главные меню, контекстные меню, клавиатурные комбинации и панели инструментов. 124 ПРИЛОЖЕНИЕ В. ПРЕДМЕТНЫЙ СЛОВАРЬ ApplicationCommands, 86, 87, 88, 90, 91, 93, 95, 97, 98, 102, 105, 107, 108, 109, 112 Background, 5, 6, 7, 8, 10, 11, 14, 27, 28, 30, 34, 36, 53, 69, 71, 72 Button, 7, 8, 10, 11, 12, 15, 17, 19, 20, 21, 22, 26, 27, 30, 31, 32, 34, 69, 70, 82, 83, 89, 101, 102, 104, 108 ButtonBase, 20, 22, 24, 89 CheckBox, 19, 20, 21, 23, 24, 45, 46, 47, 89, 99, 124 ComboBox, 42, 43, 48, 125 CommandBinding, 91, 93, 96, 97, 98, 100, 104, 105, 107 CommandParameter, 89, 108 Control, 4, 11, 14, 37, 103 Cursor, 17, 18, 77 EditingCommands, 87, 100, 102, 110 FontFamily, 11, 12, 13, 14, 15, 16 FontSize, 10, 11, 12, 14, 16, 72 FontStretch, 11 FontStyle, 11, 12 FontWeight, 10, 11, 12 Foreground, 5, 6, 8, 10, 11, 14, 27, 30, 72 GridViewColumnHeader, 22 GroupName, 24, 25 ICommand, 82, 83, 84, 85 ICommandSource, 89, 94, 95, 108, 114 IsChecked, 23, 24, 47 IsDefaulted, 22 IsThreeState, 24 ItemsControl, 42, 43 Label, 10, 13, 14, 16, 19, 20, 21, 44, 45 ListBox, 42, 43, 44, 45, 46, 47, 48 MediaCommands, 87, 102 MessageBox, 62, 66, 92, 106 MessageBoxButton, 66 MessageBoxImage, 66 NavigationCommands, 86, 102, 108 Opacity, 8, 9, 10 OpenFileDialog, 66, 67 PasswordBox, 37, 41 Popup, 33, 34, 35, 36 ProgressBar, 49, 50, 52, 53, 125 RadioButton, 19, 20, 21, 23, 24, 25, 26 RangeBase, 49, 50 RepeatButton, 22, 23 RichTextBox, 37 RoutedCommand, 83, 84, 86, 96, 98, 114 RoutedUICommand, 86, 88, 89, 99, 102, 103, 112 ScrollBar, 23, 49, 50, 125 ShowDialog(), 57, 58, 60, 65 Slider, 49, 50, 51, 52 SolidColorBrush, 5, 6, 7 125 SystemColors, 5, 6 TextBox, 10, 13, 19, 21, 22, 37, 38, 39, 40, 41, 82 TextDecorations, 13, 36 ToggleButton, 22, 23, 24 ToolTip, 26, 27, 28, 30, 31, 32, 33, 34, 95 Tooltips, 26 ToolTipService, 27, 31, 32 Typography, 13 Window, 14, 53, 54, 57, 60, 62, 63, 64, 65, 68, 69, 70, 71, 72, 75, 92, 93, 98, 104, 107 Библиотека базовых команд, 86 Источники команд, 82, 89 Классы команд, 79, 83 Команды, 79, 82, 89, 99, 101, 105 Модель команд WPF, 81 Привязки команд, 82, 90 Целевые объекты команд, 82 126 БИБЛИОГРАФИЧЕСКИЙ СПИСОК 1. Мак-Дональд М. WPF: Windows Presentation Foundation в .NET 4.0 с примерами на C# 2010 для профессионалов / М. Мак-Дональд. – М.: Вильямс, 2011. 2. Petzold С. 3D Programming for Windows®: Three-Dimensional Graphics Programming for the Windows Presentation Foundation / C. Petzold. - Microsoft Press, 2007. 3. Nathan А. Windows Presentation Foundation Unleashed. / A. Natan, D. Lehenbauer - Sams Publishing, 2007. 4. Основы проектирования интерфейсов с использованием технологии Windows Presentation Foundation / Ан. Б. Шамшев. – Ульяновск: УлГТУ, 2012. 127 ОГЛАВЛЕНИЕ ПРЕДИСЛОВИЕ................................................................................................... 3 ВВЕДЕНИЕ ............................................................................................................ 4 1. КЛАСС CONTROL ........................................................................................ 4 1.1. Кисти фона и переднего плана ............................................................. 5 1.2. Установка цветов в XAML ................................................................... 7 1.3. Прозрачность .......................................................................................... 8 1.4. Шрифты ................................................................................................ 11 1.5. Текстовые декорации и типография .................................................. 12 2. ЭЛЕМЕНТЫ УПРАВЛЕНИЯ СОДЕРЖИМЫМ .................................... 18 2.1. Метки .................................................................................................... 18 2.2. Кнопки................................................................................................... 20 2.3. Контекстные окна указателя ............................................................... 25 2.4. Настройка параметров контекстного окна указателя ...................... 28 2.5. Элемент управления Popup ................................................................. 32 2.6. Текстовые элементы управления ....................................................... 36 2.8. Другие возможности элемента управления TextBox ....................... 39 2.9. Элемент управления PasswordBox ..................................................... 40 3. ЭЛЕМЕНТЫ УПРАВЛЕНИЯ СПИСКАМИ ............................................ 41 3.1. Элемент управления ListBox .............................................................. 42 3.2. Элемент управления ComboBox......................................................... 47 4. ЭЛЕМЕНТЫ УПРАВЛЕНИЯ, ОСНОВАННЫЕ НА ДИАПАЗОНАХ ЗНАЧЕНИЙ ......................................................................................................... 47 4.1. Элемент управления Slider ................................................................. 49 4.2. Элемент управления ProgressBar ....................................................... 51 5. ОКНА ................................................................................................................ 52 5.1. Класс Window ....................................................................................... 52 5.2. Отображение окна................................................................................ 56 5.3. Позиционирование окна ...................................................................... 57 5.4. Манипулирование информацией о местоположении окна ............. 59 5.5. Владение окнами .................................................................................. 62 5.6. Модель диалогового окна ................................................................... 63 128 5.7. Встроенные диалоговые окна ............................................................. 64 5.8. Непрямоугольные окна ....................................................................... 66 6. КОМАНДЫ...................................................................................................... 76 6.1. Общие сведения о командах ............................................................... 77 6.2. Модель команд WPF............................................................................ 79 6.3. Класс RoutedCommand ........................................................................ 81 6.4. Класс RoutedUICommand и библиотека команд .............................. 83 6.5. Выполнение команд............................................................................. 86 6.6. Привязки команд .................................................................................. 87 6.7. Использование множества источников команд................................ 90 6.8. Ограничения команд WPF .................................................................. 96 6.9. Элементы управления со встроенными командами......................... 97 6.10. Усовершенствованные команды ...................................................... 99 6.11. Использование одной команды в разных местах ......................... 101 6.12. Использование параметра команды ............................................... 104 6.13. Отслеживание и отмена команд ..................................................... 105 ЗАКЛЮЧЕНИЕ ................................................................................................ 113 ПРИЛОЖЕНИЕ А. ПРИМЕРЫ ТЕСТОВЫХ ЗАДАНИЙ ...................... 114 ПРИЛОЖЕНИЕ Б. ГЛОССАРИЙ ................................................................ 123 ПРИЛОЖЕНИЕ В. ПРЕДМЕТНЫЙ СЛОВАРЬ ....................................... 125 БИБЛИОГРАФИЧЕСКИЙ СПИСОК ......................................................... 127 129 Учебное издание ШАМШЕВ Анатолий Борисович КЛАССИЧЕСКИЕ ЭЛЕМЕНТЫ ПОЛЬЗОВАТЕЛЬСКОГО ИНТЕРФЕЙСА В WINDOWS PRESENTATION FOUNDATION Учебное пособие Лр № 020640 от 22.10.97. РедакторН. А. Евдокимова Подписано в печать 23.03.2012. Бумага писчая. Усл. печ. л. 7,21. Тираж 125 экз. Заказ 200. Ульяновский государственный технический университет, 432027, г. Ульяновск, Сев. Венец, д.32. Типография УлГТУ, 432027, г. Ульяновск, Сев. Венец, д.32.