Все необходимые для выполнения данной работы программы можно найти в прилагаемом каталоге.
В WPF используются элементы управления двух видов:
ContentControl. Такой элемент может
содержать в себе только один ближайший вложенный дочерний элемент.ItemsControl. Такие
элементы являются элементами контейнерного типа и могут содержать любое количество
ближайших вложенных дочерних элементов.В WPF структуру приложения можно рассматривать как дерево элементов. Если
приложение формируется в XAML, то дерево создается на основе C# дерево создается с
учетом того, как строятся коллекции для контейнеров, производных от ItemsControl,
и задается содержимое элементов-потомков ContentControl.
В WPF существуют три взгляда на дерево элементов:
Различия между логическими и визуальными деревьями не всегда важны
до тех пор, пока не потребуется более тонкое управление подсистемами WPF.
Эти две концепции позволяют понять, как могут повлиять на конечный результат
те или иные изменения, вносимые в разметку или код.
Логическое дерево представляет собой модель структуры зависимостей
родитель-потомок, определяемую вложенностью содержимого крупных элементов при
формировании разметки XAML. Это то, что мы можем представить себе на этапе
проектирования иерархии имеющихся элементов. На этапе компиляции часть элементов
детализируется в более мелкие.
Визуальное дерево представляет
модель отображаемых элементов интерфейса, порожденных классом Visual. При
компиляции приложения логическое дерево расширяется до визуального, поскольку
часть укрупненных элементов разбивается на атомарные.
Введение перенаправленных событий в WPF связано с принятой моделью содержимого.
Простого события для элемента стало недостаточно, поскольку он может содержать
в себе еще несколько визуальных элементов. Тогда для полного контроля над событием
контейнерного элемента пришлось бы привязывать один и тот же обработчик еще
и к событиям каждого из дочерних элементов. Всплывающее событие решает эту
проблему, оно восходит и ищет элемент с подписавшимся на него обработчиком.
Например, если кнопка в качестве содержимого имеет несколько элементов: надпись,
рисунок или другую кнопку, то щелчок курсором мыши на дочернем элементе еще
не означает щелчок на самой кнопке. Событие щелчка возбуждается дочерним для
кнопки элементом и его нужно преобразовать в событие щелчка для самой
кнопки. В этом случае срабатывает механизм маршрутизации событий ( RoutedEvent ).
Здесь ярко проявляются различия в логических и визуальных деревьях, поскольку
события на низком уровне маршрутизируются на основе визуального
дерева.
Разработчикам приложения не всегда необходимо знать, реализуется ли обрабатываемое событие как перенаправленное. Перенаправленные события имеют особое поведение, но оно остается невидимым, если событие обрабатывается на элементе, где оно возникает. Поэтому для традиционного использования различия между старым и новым механизмами работы событий несущественны.
Маршрутизированные события поддерживают
следующие стратегии маршрутизации ( RoutingStrategy ):
Direct - обрабатывается на источнике, возбудившем событиеTunnel (тоже самое - тоннельную!) - нисходящая маршрутизация
событийBubble - восходящая маршрутизация событийПрямые события ведут себя как
обычные события в .NET Framework. У нас есть три инградиента при рассмотрении
этого рода событий: элемент, событие и прикрепленный обработчик (или
несколько обработчиков для делегата события). При регистрации обработчика все
три инградиента жестко связываются. Событие элемента возбуждается только на
нем и может быть обработано только собственными обработчиками.
Туннельные события могут быть возбуждены
любым элементом. Но они всегда начинаются на корневом элементе ( Window, Page )
и передвигаются вниз по дереву элементов, пока не будут обработаны и прерваны ( остановлены )
каким-нибудь из них или не достигнут исходного элемента-источника для события.
Это позволяет находящимся выше элементам перехватить событие и обработать его
прежде, чем оно достигнет возбудившего его элемента. К именам всех туннельных
событий добавлена приставка Preview (например, PreviewMouseDown ), поэтому их
иногда еще называют событиями предварительного просмотра.
Пузырьковые события будут всплывать
вверх (распространяться) по визуальному дереву от исходного элемента, пока
не будут обработаны или не достигнут корневого элемента. Эти события распространяются
в противоположном к туннельным направлении, что позволяет обработать их
любому родительскому элементу, стоящему в логическом дереве выше исходного.
Например, обработчик события MouseDown можно прикрепить к охватывающему элементу
сетки, а не к самому элементу внутри сетки, или к корневому окну. Пузырьковые
события имеют имена, просто указывающие их действие (например, MouseDown ).
Перенаправленные события можно еще называть векторными, в отличие от обычных скалярных. Перенаправленные события могут использоваться для связи элементов дерева, так как данные событий можно менять и они сохраняются для каждого элемента в маршруте. Один элемент может изменить что-либо в данных события и это изменение будет доступно для следующего элемента в маршруте. Поэтому передача вверх перенаправляемых событий обычно используется для получения отчета об изменении входных данных или о состоянии различных элементов управления пользовательского интерфейса.
Прослушиватели перенаправленных событий и источники перенаправленных событий
не требуют для совместного использования общего события в их иерархии. Любой UIElement или ContentElement может являться прослушивателем перенаправленных
событий. Поэтому перенаправленные события можно рассматривать как
"концептуальный интерфейс", посредством которого разнородные элементы в приложении
могут обмениваться данными. Эта концепция для перенаправленных
событий особенно применима для событий ввода.
Существует разновидность пузырьковых событий, которые называются вложенными.
Большинство контейнерных элементов (например, Grid ) не имеет события Click,
потому что оно для них неконкретизировано. Однако к ним все равно можно прикрепить
(присоединить) обработчик наподобие свойства зависимости, позаимствовав событие
у дочернего элемента, который способен это событие возбудить. Это не значит,
что элемент с несвойственным ему вложенным событием будет способен его возбуждать,
но возбужденное другими и движущееся через него событие он будет способен
перехватить и обработать.
Обработчик события рекомендуется прикреплять в разметке XAML, поскольку такой
способ поддерживает и обычные и маршрутизируемые события. Синтаксис прикрепления
аналогичен определению свойств. В дескрипторе элемента мы пишем имя события
и через знак присваивания - имя обработчика, а оболочка в ответ сама сгенерирует
заготовку метода с соответствующей событию сигнатурой. Синтаксис вложенных
событий аналогичен, но имеет префикс элемента зависимости, например
<Grid ButtonBase.Click="Grid_Click">
<Button Click="Button_Click">
Генератор Click
</Button>
</Grid>
В такой конструкции при щелчке на кнопке Button возбудится событие Click.
Это событие начнет всплывать к корню визуального дерева. Прежде всего оно будет
обработано самой кнопкой, потому что мы прикрепили обработчик Button_Click.
Затем оно будет обработано обработчиком Grid_Click, поскольку мы его тоже предусмотрели.
И так далее, пока не достигнет корня визуального дерева. Даже если у кнопки Button не предусмотреть обработчик, то событие все равно будет ею возбуждено
при щелчке на ней и начнет всплывать по дереву элементов.
Под маршрутизацией событий подразумевается движение событий (event bubbling)
по дереву элементов в поиске своих обработчиков. Маршрутитизацию событий еще
называют перенаправлением событий. Механизм этого движения можно рассмотреть
на примере событий PreviewMouseDown (туннельное) и MouseDown (пузырьковое).
При щелчке курсором мыши на каком-нибудь элементе окна WPF (назовем его целевым
- target) вначале возбуждается туннельное событие PreviewMouseDown, которое
движется от корня дерева к целевому элементу. Когда оно достигнет корня дерева,
на смену ему возбуждается парное событие MouseDown, которое начинает движение
в противоположном направлении: от целевого элемента к корню дерева.
В любом элементе по маршруту
движения этих событий мы можем прикрепить соответствующий обработчик, перехватить
событие и обработать его (а затем, если нужно, остановить). Прикрепленному
обработчику вместе с событием передается объект аргументов (обычно вторым параметром).
Для событий PreviewMouseDown и MouseDown это будет объект e типа MouseButtonEventArgs, для события Click это будет объект e типа RoutedEventArgs, для события KeyDown - объект e типа KeyEventArgs, и так далее. Каждый из этих объектов имеет булево свойство Handled,
с помощью которого в любом из обработчиков можно прервать (остановить) дальнейшую
маршрутизацию события, присвоив значение e.Handled=true.
Если по пути к целевому элементу
мы останавливаем туннельное событие, то не будет возбуждаться и парное к
нему пузырьковое событие. Дальнейшая маршрутизация событий может автоматически
прерываться и другими событиями. Так например, при щелчке на кнопке вначале
возбуждается туннельное событие PreviewMouseDown, которое движется от корня
к кнопке. По достижении кнопки парное событие MouseDown не возбуждается, а
вместо него начинает всплывать сгенерированное кнопкой событие Click.
Более
того, если кнопка внутри себя содержит дочерние элементы и щелчок выполнен
по одному из них, то событие PreviewMouseDown доходит до этого целевого элемента.
Затем возбуждается парное пузырьковое событие MouseDown, но поднявшись до элемента
кнопки оно подавляется и заменяется всплывающим событием Click. Кнопка сама
устанавливает для события MouseDown флаг Handled=true и
возбуждает собственное событие щелчка. Это действие соответствует наиболее
естественному поведению кнопки.
Если не считать исключений, которые возбуждаются средой исполнения, то библиотечные
события WPF делятся на 4 группы:
События времени жизни всех элементов управления наследуются от класса FrameworkElement и указаны в таблице
| Событие | Описание |
|---|---|
Initialized |
Данное событие является обычным, а не перенаправленным, и возникает после
создания элемента и определения всех его полей и свойств. Такое состояние
характеризуется булевым свойством IsInitialized=true. На этом этапе еще
не применены стили и привязка данных. Соседние элементы окна могут быть
еще не полностью созданными |
Loaded |
Возникает после события Initialized, когда все окно закончило инициализацию
и дополнительно были применены стили и привязка данных к элементу. Это
последний этап, за которым следует визуализация элемента. В этот момент
он принимает состояние IsLoaded=true |
Unloaded |
Возбуждается сразу после удаления элемента из контейнера или его закрытия |
Класс FrameworkElement наследует и реализует интерфейс ISupportInitialize,
объявляющий методы BeginInit() и EndInit(). Эти методы автоматически
вызываются анализатором разметки, но если создавать элементы в процедурном
коде, то их следует вызывать вручную. После вызова метода EndInit() инициализация
элемента считается завершенной и возбуждается сигнализирующее событие Initialized.
Любой составной объект, имеющий ссылки на другие объекты, оформленные в нем
как свойства типов адресуемых объектов, начинает свое создание
с переферийных объектов. Эту нужно для того, чтобы при создании самого родителя
его свойствам можно было присвоить уже готовые адреса вспомогательных объектов.
Дерево элементов WPF тоже начинает строиться с листьев, то есть глубоко вложенные
элементы инициализируются до того, как будут инициализированы их родительские
контейнеры.
Есть еще события времени жизни окна:
| Событие | Описание |
|---|---|
Activated |
Возбуждается при получении окном фокуса ввода системы. По функциональности эквивалентно событию GotFocus элемента управления |
Closed |
Возбуждается после закрытия окна, когда еще объекты дерева являются
доступными и их событие Unloaded еще не возникло |
Closing |
Возникает при получении команды на закрытие окна, которую еще ее можно
отменить присвоением в обработчике свойству Cancel=true через аргумент CancelEventArgs. Если пользователь не закрывал окно, а просто вышел из
системы, то нужно обрабатывать событие уровня приложения System.Windows.Application.SessionEnding |
ContentRendered |
Возникает сразу после первой визуализации окна |
Deactivated |
Возбуждается каждый раз, когда окно теряет фокус ввода при свертывании или переключении на другое окно |
LocationChanged |
Происходит, когда местоположение окна изменяется |
SourceInitialized |
Предусмотрено для поддержки взаимодействия с Win32 |
StateChanged |
Происходит при изменении окном свойства WindowState |
События ввода через объекты-аргументы своих обработчиков передают информацию о состоянии устройств ввода при манипулировании с ними пользователей. Анализ этой информации позволяет принять решение об адекватной реакции приложения. События ввода являются маршрутизируемыми.
При нажатии клавиши пользователем, когда элемент имеет фокус, возникает несколько
событий, которые определены в классе UIElement и в порядке их возбуждения приведены
в таблице
| Событие | Стратегия маршрутизации | Описание |
|---|---|---|
PreviewKeyDown |
Tunnel | Происходит при нажатии |
KeyDown |
Bubble | Происходит при нажатии |
PreviewTextInput |
Tunnel | Происходит для символьных клавиш, когда нажатие завершено и элемент получил символ |
TextInput |
Bubble | Происходит для символьных клавиш, когда нажатие завершено и элемент получил символ |
PreviewKeyUp |
Tunnel | Происходит при отпускании |
KeyUp |
Bubble | Происходит при отпускании |
Некоторые элементы сами обрабатывают часть этих событий. Они могут блокировать
дальнейшее продвижение некоторых из них или возбуждать дополнительные события.
Например, элемент TextBox блокирует событие TextInput, а для клавиш-стрелок
блокирует и событие KeyDown, но в то же время возбуждает свое событие TextChanged при изменении содержимого поля ввода.
Построим приложение, которое продемонстрирует обработку клавиатурных событий.
Создайте решение EventsAndCommands вместе
с новым WPF -проектом KeyEvents командой File/New/Project, для этого настройте
окно мастера так
Обратите внимание на выбор версии библиотеки .NET Framework 3.0,
при более низких версиях шаблоны для WPF станут недоступными. Стартовым новый
проект тоже становится автоматически, поскольку он пока первый и единственный
в решении. Дерево Solution Explorer полужирным шрифтом.
Заполните файл разметки Window1.xaml следующим дескрипторным кодом<Window x:Class="KeyEvents.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: События клавиатуры"
Height="300"
Width="300"
MinHeight="300"
MinWidth="300"
Background="#FFD4D0C8"
>
<DockPanel LastChildFill="True" Margin="2,2,2,0">
<DockPanel DockPanel.Dock="Top" LastChildFill="True">
<Label DockPanel.Dock="Left" Content="Введите текст:" />
<TextBox Name="textBox"
PreviewKeyDown="textBox_KeyEvent"
KeyDown="textBox_KeyEvent"
PreviewKeyUp="textBox_KeyEvent"
KeyUp="textBox_KeyEvent"
PreviewTextInput="textBox_PreviewTextInput"
/>
</DockPanel>
<StackPanel DockPanel.Dock="Bottom" Margin="0,5,0,0">
<CheckBox Name="checkIgnoreRepeat"
Content="Игнорировать автогенерацию клавиш"
IsChecked="True"
Click="Check_Click"
IsTabStop="False"
/>
<CheckBox Name="checkIgnorePreviewTextInput"
Content="Игнорировать событие PreviewTextInput"
IsChecked="True"
Margin="0,2"
Click="Check_Click"
IsTabStop="False"
/>
<CheckBox Name="checkIgnoreSymbol"
Content="Запретить в TextBox нечисловые клавиши"
Margin="15,2"
Click="Check_Click"
IsTabStop="False"
/>
<CheckBox Name="checkIgnoreOther"
Content="Запретить в TextBox некоторые клавиши"
Margin="0,2"
Click="Check_Click"
IsTabStop="False"
/>
<CheckBox Name="checkConvertNumber"
Content="Конвертировать вывод цифровых клавиш"
Margin="0,2"
Click="Check_Click"
IsTabStop="False"
/>
<Button
HorizontalAlignment="Right"
Content="Очистить"
Margin="0,5,0,2"
Padding="5,0,5,0"
Click="Button_Click"
/>
</StackPanel>
<ListBox Name="listBox" Focusable="False" />
</DockPanel>
</Window>
Пройдитесь по разметке
и командой Navigate to Event Handler контекстного меню для
записей событий создайте заготовки обработчиков в файле процедурного
кодаНекоторые события связаны с одними и теми же обработчиками, но лишних обработчиков
мы таким образом все равно не создадим. Команда Navigate to Event Handler создает новый обработчик только тогда, когда он еще не существуют, а иначе
приведет только к позиционированию на уже существующий.
Запустите проект и
убедитесь, что приведенная разметка реализует следующий интерфейс окна приложения
Без создания обработчиков для указанных в разметке событий мы бы не смогли откомпилировать приложение и получить приведенное окно. Если в разметке для какого-то события зарегистрирован обработчик, то перед запуском приложения его нужно обязательно создать, пусть даже с пустым телом. Поскольку компилятор будет настойчиво искать его определение в файле процедурного кода и выдаст ошибку в случае неуспеха. Другое дело, когда обработчик создан, но не присоединен к событию. В таком случае он считается одним из методов класса окна и компилятор протестовать не будет.
Изучите код файла
разметки, обеспечивающий требуемую компоновку элементов
управления пользовательского интерфейса нашего упражненияСледующим шагом мы должны заполнить каждый из обработчиков соответствующим кодом. Но вместо того, чтобы делать это по частям, приведем весь код сразу.
Удалите из
файла Window1.xaml.cs процедурного кода окна все что там есть и заполните
его следующим содержимым, которое полностью согласуется с разметкойusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace KeyEvents
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
textBox.Focus();
}
private void textBox_KeyEvent(object sender, KeyEventArgs e)
{
if ((bool)checkIgnoreRepeat.IsChecked e.IsRepeat)
return;// Игнорировать повторные события
// Запретить действие в TextBox некоторых клавиш
if(checkIgnoreOther.IsChecked.Value)
switch (e.Key)
{
case Key.Space: // Пробел
case Key.Left: // Стрелка влево
case Key.Right: // Стрелка вправо
case Key.Home: // В начало поля
case Key.End: // В конец поля
e.Handled = true;
break;
}
string key = e.Key.ToString();
// Конвертируем вывод цифровых клавиш основной клавиатуры
if (checkConvertNumber.IsChecked.Value)
{
KeyConverter converter = new KeyConverter();
key = converter.ConvertToString(e.Key);
}
string message = e.RoutedEvent.ToString();
message = message.Substring(message.IndexOf('.') + 1);
message = String.Format("Event: {0,-25}", message) +
"\t Key: " + key;
listBox.Items.Add(message);
listBox.ScrollIntoView(message);// Видеть последний
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// Очищаем поле и список
textBox.Clear();
listBox.Items.Clear();
// Возвращаем фокус
textBox.Focus();
}
private void Check_Click(object sender, RoutedEventArgs e)
{
// Распознаем и синхронизируем взаимосвязанные CheckBox
FrameworkElement checkBox = e.Source as FrameworkElement;
switch (checkBox.Name)
{
case "checkIgnoreSymbol":
if (checkIgnoreSymbol.IsChecked.Value)
checkIgnorePreviewTextInput.IsChecked = false;
break;
case "checkIgnorePreviewTextInput":
if (checkIgnorePreviewTextInput.IsChecked.Value)
checkIgnoreSymbol.IsChecked = false;
break;
}
// Возвращаем фокус
textBox.Focus();
}
// Для отображаемых символов текстового поля
private void textBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (checkIgnorePreviewTextInput.IsChecked.Value)
return;
// Запрещаем в TextBox нечисловые символы
short val;
// Попытка преобразовать в число без генерации исключения
bool success = Int16.TryParse(e.Text, out val);
if (!success)
{
e.Handled = true;// Останавливаем событие
}
string message = e.RoutedEvent.ToString();
message = message.Substring(message.IndexOf('.') + 1);
message = String.Format("Event: {0,-25}", message) +
"\t Text: " + e.Text;
listBox.Items.Add(message);
}
}
}
В приведенном процедурном коде перехватываются события клавиатуры и в список выводится информация о нажатых клавишах, которая передается вместе с событием в обработчики через объекты их аргументов. Целевым объектом клавиатурных событий в данном коде является элемент текстового поля. Интерфейсные переключатели обеспечивают смену режимов обработки клавиатурных событий и иллюстрируют возможности библиотечных классов.
Запустите и испытайте
работу приложения, один из рабочих моментов которого может выглядеть так
Изучите приведенный
код процедурного файла, обратите внимание на некоторые приемы программированияВ обработчиках мы извлекаем информацию о событиях как RoutedEvent. Это значит,
что события клавиатуры маршрутизируемые и их можно прослушивать и перехватывать
не только в целевом объекте, но и в других местах логического дерева элементов.
Убедимся в этом:
В файле разметки переместите
(вырежьте из... и вставьте в...) весь блок регистрации событий из элемента TextBox в самый внешний контейнер DockPanel, чтобы схематично окончательный
код стал таким<Window x:Class="KeyEvents.Window1"
........................................................
>
<DockPanel LastChildFill="True" Margin="2,2,2,0"
PreviewKeyDown="textBox_KeyEvent"
KeyDown="textBox_KeyEvent"
PreviewKeyUp="textBox_KeyEvent"
KeyUp="textBox_KeyEvent"
PreviewTextInput="textBox_PreviewTextInput"
>
<DockPanel DockPanel.Dock="Top" LastChildFill="True">
<Label DockPanel.Dock="Left" Content="Введите текст:" />
<TextBox Name="textBox"
/>
</DockPanel>
<StackPanel DockPanel.Dock="Bottom" Margin="0,5,0,0">
.....................................................
</StackPanel>
<ListBox Name="listBox" Focusable="False" />
</DockPanel>
</Window>
Запустите приложение
и убедитесь, что функциональность его осталась прежнейПоставляемый событием в его обработчик объект e класса KeyEventArgs содержит не только информацию о нажатой клавише, но и сведения
о состоянии модификаторов (расширителей), таких как Shift, Ctrl, Alt. Свойство e.KeyStates информирует о том, в каком состоянии находилась клавиша в момент
генерации события: Down, None, Toggled.
Много полезной информации имеет свойство e.KeyboardDevice,
представляющее экземпляр класса KeyboardDevice. Из его свойства e.KeyboardDevice.FocusedElement можно узнать, какой элемент в данный момент имеет фокус клавиатурного ввода.
Из свойства e.KeyboardDevice.Modifiers объекта-аргумента обработчика можно
определить состояние
private void KeyEvent(object sender, KeyEventArgs e)
{
if ((e.KeyboardDevice.Modifiers ModifierKeys.Control)
== ModifierKeys.Control)
Console.WriteLine("Нажато расширение Ctrl");
// Или аналогичный код
if((e.KeyboardDevice.Modifiers ModifierKeys.Alt) > 0)
Console.WriteLine("Нажато расширение Alt");
// Или аналогичный код
if ((e.KeyboardDevice.Modifiers ModifierKeys.Shift) != 0)
Console.WriteLine("Нажато расширение Shift");
}
Объект e.KeyboardDevice имеет ряд полезных методов для прослушивания состояния
любой интересующей нас клавиши
Методы принимают клавишу, возбудившую событие, и сообщают, что с ней происходило в момент возникновения события. Вот пример
private void KeyEvent(object sender, KeyEventArgs e)
{
if (e.KeyboardDevice.IsKeyDown(e.Key))
Console.WriteLine("Клавиша {0} нажата", e.Key);
if (e.KeyboardDevice.IsKeyUp(e.Key))
Console.WriteLine("Клавиша {0} отпущена", e.Key);
// Попутно проверяем
if(e.KeyboardDevice.IsKeyToggled(Key.NumLock))
Console.WriteLine("Клавиша NumLock включена");
}
Не только в момент возникновения клавиатурного ввода мы можем получать информацию
о состоянии клавиатуры в обработчике, но и в любой интересующий нас момент
времени в любом месте программы. Для этого достаточно воспользоваться статическим
классом System.Windows.Input. Keyboard, который отслеживает клавиатуру
компьютера. По своим возможностям он значительно мощнее класса KeyboardDevice,
который мы получаем в обработчике вместе с клавиатурным событием. Вот пример
// Где-то в программе
bool Win, Ctrl, Alt, NumLock, CapsLock, ScrollLock;
Win = (Keyboard.Modifiers ModifierKeys.Windows) > 0;
Ctrl = (Keyboard.Modifiers ModifierKeys.Control) > 0;
Alt = (Keyboard.Modifiers ModifierKeys.Alt) > 0;
NumLock = Keyboard.IsKeyToggled(Key.NumLock);
CapsLock = Keyboard.IsKeyToggled(Key.Capital);
ScrollLock = Keyboard.IsKeyToggled(Key.Scroll);
В классе любого элемента управления WPF предусмотрено достаточно событий для
программного реагирования на действия мышью со стороны пользователя. Все такие
события имеют информативные названия со вставкой Mouse, например:
PreviewMouseDown, MouseDown, PreviewMouseUp, MouseUp, PreviewMouseMove, MouseMove, PreviewMouseWheel, MouseWheel, PreviewDragEnter, DragEnter, PreviewDrop, Drop и т.д.
Большинство событий унаследовано интерфейсными элементами WPF от класса UIElement,
но часть событий добавлена самостоятельно или другими классами. Так, более
поздний в цепочке наследования класс Control добавляет события PreviewMouseDoubleClick и MouseDoubleClick. Всеразличная информация о состоянии мыши передается вместе
с событием в обработчик через объект аргумента и может быть из него извлечена.
Но также, как и в случае с клавиатурным классом Keyboard, статический класс Mouse следит за состоянием мыши в реальном масштабе времени.
Все события мыши, связанные со щелчками или перемещением, передают объект
аргументов MouseButtonEventArgs, наследующий класс MouseEventArgs. В этом
объекте содержится информации о текущих координатах курсора, кнопке мыши, которая
произвела щелчок (левая/правая/средняя), состоянии кнопки (нажата/отпущена),
какой щелчок (одинарный/двойной) и многое другое. Даже если в элементе нет
события MouseClick или MouseDoubleClick, его можно легко распознать в
обработчике события MouseDown, проанализировав свойство аргумента ( MouseButtonEventArgs
e ) как e.ClickCount == 1 (одинарный щелчок) или e.ClickCount
== 2 (двойной
щелчок).
Событий мыши очень много и каждое из них имеет свою специфику, поэтому ограничимся только таким вступительным словом и перейдем к их практическому использованию.
Добавьте к решению
командой File/Add/New Project новый проект с именем ListenerEvents и
назначьте его стартовым
Выполните команду Project/ListenerEvents Properties... и настройте выпадающий список Output type на значение Console Application,
чтобы параллельно запускались графическое и консольное окна приложения
Мы будем воздействовать мышью на графическое окно, а вывод обработчиков событий наблюдать в консольном окне. Порядок выполнения обработчиков позволит проследить маршрут движение событий по дереву элементов.
Заполните файл разметки Window1.xaml следующим кодом<Window x:Class="ListenerEvents.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Маршрутизация событий" Height="300" Width="300"
Background="Red"
ToolTip="Элемент Window - Red"
PreviewMouseDown="Window_PreviewMouseDown"
MouseDown="Window_MouseDown"
ButtonBase.Click="Window_Click"
>
<Window.ContextMenu>
<ContextMenu>
<MenuItem Header="Item1" />
</ContextMenu>
</Window.ContextMenu>
<DockPanel>
<Menu
DockPanel.Dock="Top"
ToolTip="Элемент Menu - #FFD4D0C8"
Background="#FFD4D0C8"
>
<MenuItem Header="File">
<MenuItem Header="_Open" />
<MenuItem Header="_Save" />
<MenuItem Header="Save_As" />
<MenuItem Header="E_xit" />
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Header="Cu_t" />
<MenuItem Header="_Copy" />
<MenuItem Header="_Paste" />
</MenuItem>
</Menu>
<Grid
Width="220"
Height="200"
Background="Green"
ToolTip="Элемент Grid - Green"
PreviewMouseDown="Grid_PreviewMouseDown"
MouseDown="Grid_MouseDown"
ButtonBase.Click="Grid_Click"
>
<UniformGrid
Rows="3"
Height="140" Width="130"
Background="Blue"
ToolTip="Элемент UniformGrid - Blue"
PreviewMouseDown="UniformGrid_PreviewMouseDown"
MouseDown="UniformGrid_MouseDown"
ButtonBase.Click="UniformGrid_Click"
>
<TextBlock
Background="Yellow"
VerticalAlignment="Center"
TextAlignment="Center"
ToolTip="Элемент TextBlock - Yellow"
PreviewMouseDown="TextBlock_PreviewMouseDown"
MouseDown="TextBlock_MouseDown"
ButtonBase.Click="TextBlock_Click"
>
Туннельное
<LineBreak />
Пузырьковое
</TextBlock>
<TextBlock
Background="Aqua"
VerticalAlignment="Center"
TextAlignment="Center"
ToolTip="Элемент TextBlock - Aqua"
MouseEnter="TextBlock_MouseEnter"
MouseLeave="TextBlock_MouseLeave"
PreviewMouseDown="TextBlock_PreviewMouseDown"
MouseDown="TextBlock_MouseDown"
ButtonBase.Click="TextBlock_Click"
>
Прямое MouseEnter
<LineBreak />
Прямое MouseLeave
</TextBlock>
<Button
Background="Orange"
VerticalAlignment="Center"
ToolTip="Элемент Button - Orange"
PreviewMouseDown="Button_PreviewMouseDown"
MouseDown="Button_MouseDown"
Click="Button_Click"
>
Генератор Click
</Button>
</UniformGrid>
</Grid>
</DockPanel>
</Window>
Обратите внимание, что все элементы, пока, мы сделали неименованными, но среда
выполнения в точности определит, какой элемент возбудил или обработал событие.
В названиях пунктов меню мы применили знаки подчеркивания для использования
горячих клавиш, которые проявятся после нажатия клавиши Alt в работающем приложении.
В Windows Forms для этой цели используется символ амперсанда , но в XAML он конфликтовал бы с подобным управляющим символом , поэтому был заменен на
подчеркивание.
Заполните файл поддержки
разметки Window1.xaml.cs следующим кодомusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace ListenerEvents
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
int count;
private void Window_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
count = 0;
Console.Clear();
if (e.ChangedButton == MouseButton.Left)
Console.WriteLine("{0}) Window: Наблюдаю туннельное событие PreviewMouseDown", ++count);
else
e.Handled = true;
}
private void Window_MouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) Window: Наблюдаю пузырьковое событие MouseDown", ++count);
}
private void Window_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine("{0}) Window: Наблюдаю пузырьковое событие Click (как вложенное)", ++count);
}
private void Grid_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) Grid: Наблюдаю туннельное событие PreviewMouseDown", ++count);
}
private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) Grid: Наблюдаю пузырьковое событие MouseDown", ++count);
}
private void Grid_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine("{0}) Grid: Наблюдаю пузырьковое событие Click (как вложенное)", ++count);
}
private void UniformGrid_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) UniformGrid: Наблюдаю туннельное событие PreviewMouseDown", ++count);
}
private void UniformGrid_MouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) UniformGrid: Наблюдаю пузырьковое событие MouseDown", ++count);
}
private void UniformGrid_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine("{0}) UniformGrid: Наблюдаю пузырьковое событие Click (как вложенное)", ++count);
}
private void TextBlock_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) TextBlock: Наблюдаю туннельное событие PreviewMouseDown", ++count);
}
private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) TextBlock: Наблюдаю пузырьковое событие MouseDown", ++count);
}
private void TextBlock_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine("{0}) TextBlock: Наблюдаю пузырьковое событие Click (как вложенное)", ++count);
}
private void TextBlock_MouseEnter(object sender, MouseEventArgs e)
{
Console.WriteLine("{0}) TextBlock: Возбуждаю прямое событие MouseEnter", ++count);
}
private void TextBlock_MouseLeave(object sender, MouseEventArgs e)
{
Console.WriteLine("{0}) TextBlock: Возбуждаю прямое событие MouseLeave", ++count);
}
private void Button_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) Button: Наблюдаю туннельное событие PreviewMouseDown", ++count);
}
private void Button_MouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) Button: Наблюдаю пузырьковое событие MouseDown", ++count);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine("{0}) Button: Возбуждаю пузырьковое событие Click", ++count);
}
}
}
Обратите внимание, что в первом обработчике мы перехватываем туннельное событие щелчка мыши сразу же в корневом элементе и подавлем его для правой кнопки. В то же время это не влияет на работу контекстного меню, поскольку в этом случае среда выполнения создает свое всплывающее окно и передает ему фокус ввода.
Запустите приложение
- получим следующее графическое окно
Для элементов логического дерева, входящих в разметку, мы определили разный
цвет фона, чтобы можно было их визуально различать. Цвет фона меню Background="#FFD4D0C8" мы
назначили в стиле HTML для напоминания, хотя
по умолчанию оно и так имеет такой фоновый системный цвет, типичный
для большинства пользовательских элементов управления (первый байт FF определяет
коэффициент непрозрачности Opacity, FF - полная непрозрачность, 0 - полная
прозрачность).
Теперь осталось последовательно щелкать на отдельные элементы графического окна и в консольном окне наблюдать за маршрутизацией событий по последовательности срабатывания обработчиков. Для удобства можно пользоваться всплывающей подсказкой. Ниже приведены результаты, для группировки которых использован цвет фона элемента:
| Цвет (элемент) | Вывод обработчиков |
|---|---|
| Red | |
| Green | |
| Blue | |
| Yellow | |
| Aqua | |
| Orange |
Обратите внимание, что хотя мы и вложили в элементы TextBlock событие Click,
но оно не проходит через них при щелчке на кнопке, поскольку сразу всплывает
к родительскому элементу UniformGrid. И любой из элементов с вложенным событием
Click, как и вообще с любым вложенным событием, способен его только слушать
и обрабатывать, но никоим образом не может его возбуждать.
Может возникнуть вопрос, а как на самом деле в коде происходит регистрация
обработчиков событий, если мы их прикрепили в разметке, ...и с помощью каких
делегатов? Все это делает за нас оболочка благодаря тому, что класс поддержки
разметки объявлен как partial (частичный). Чтобы увидеть это, надо зайти
в конструктор кодовой части файла Window1.axml.cs для класса Window1,
щелкнуть правой кнопкой мыши на вызове метода InitializeComponent() и
выбрать команду Go To Definition (Перейти к определению) из контекстного
меню. В редакторе отобразится созданный файл кода Window1.g.i.cs, где и будут
полные определения прикрепленных к элементам обработчиков событий.
В нашем примере мы применили разные типы элементов для перехвата события.
Когда применяются одинаковые типы элементов, то для правильной адресации
следует присоединять
к одному и тому же событию обработчики с уникальными именами. Но можно ко всем
элементам присоединить и общий обработчик, тогда нужно присваивать
уникальные имена уже элементам и анализировать в обработчике источник, возбудивший
событие с присоединенным обработчиком. Сказанное можно проиллюстрировать примером
из MSDN для трех именованных кнопок:
private void CommonClickHandler(object sender, RoutedEventArgs e)
{
FrameworkElement feSource = e.Source as FrameworkElement;
switch (feSource.Name)
{
case "YesButton":
// do something here ...
break;
case "NoButton":
// do something ...
break;
case "CancelButton":
// do something ...
break;
}
e.Handled=true;
}
Это распространенный прием для распознавания и обыкновенных событий C#.
Наподобие библиотечных маршрутизированных событий мы можем создавать свои собственные перенаправленные события, которые называют пользовательскими. Порядок добавления в класс событий таков:
RoutedEvent,
которое будет являться базовым при упаковке события. В соответствии с соглашением,
имя поля правильно заканчивать постфиксом Event. Имя базового поля
ассоциируется с идентификатором поля как ссылкой на объект RoutedEvent, а
не с именем создаваемого событияRegisterRoutedEvent() класса System.Windows. EventManager событие нужно зарегистрировать в среде исполнения CLR (Common Language Runtime)
и сохранить ссылку на объект события в статическом поле. В дальнейшем этот
объект можно передавать обработчикам события. Вызов метода регистрации можно
разместить в статическом конструкторе класса
или вызвать сразу при инициализации поляRoutedEventHandler, использовав
расширенный способ объявления события, который используется для упаковки
поля события. Делегат обеспечивает стандартную сигнатуру для обработчиковOnОбъявление класса регистрации событий выглядит так
public static RoutedEvent RegisterRoutedEvent(
string name,
RoutingStrategy routingStrategy,
Type handlerType,
Type ownerType
)
name - имя маршрутизируемого события. Имя должно быть уникальным для данного
типа владельца и не может быть пустой строкой или иметь значение ссылки
nullroutingStrategy - стратегия маршрутизации события, заданная в качестве
значения перечисления System.Windows.RoutingStrategy. Это перечисление содержит
элементы Tunnel, Bubble и DirecthandlerType - тип обработчика событий, который должен быть типом делегата
и не может иметь значение ссылки nullownerType - тип класса владельца маршрутизируемого события, который не
может иметь значение ссылки nullМетод регистрации возвращает объект вновь зарегистрированного маршрутизируемого
события, который нужно сохранить в статическом поле и в далнейшем упаковать
в само событие. Упаковка события нужно для присоединения обработчиков, когда
обработчики назначаются в разметке. Для прикрепления обработчиков в процедурном
коде нужно использовать метод UIElement.AddHandler() явно, как будет показано
ниже.
В данном упражнении создадим в классе собственное перенаправленное событие,
которое будет исполнять назначенную стратегию маршрутизации ( Bubble, Tunnel, Direct ).
Добавьте к решению
командой File/Add/New Project новый проект с именем UserEvents и назначьте
его стартовым
Заполните файл Window1.xaml следующей базовой разметкой пользовательского интерфейса<Window x:Class="UserEvents.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Пользовательские Routed-события" Height="300" Width="300"
Background="Red"
ToolTip="Элемент Window - Red"
MinWidth="300"
MinHeight="300"
ResizeMode="CanResizeWithGrip"
>
<DockPanel>
<Grid
Width="220"
Height="200"
Background="Green"
ToolTip="Элемент Grid - Green"
>
<UniformGrid
Height="140" Width="130"
Background="Blue"
ToolTip="Элемент UniformGrid - Blue"
>
<Button
VerticalAlignment="Center"
ToolTip="Элемент Button - #FFD4D0C8"
Margin="5,0,5,0"
>
Возбудить событие
</Button>
</UniformGrid>
</Grid>
</DockPanel>
</Window>
Чтобы не мудрствовать лукаво, на данном этапе мы взяли пустой пользовательский интерфейс из предыдущего упражнения и немного его упростили, убрав меню и контекстное меню окна.
Запустите приложение
- получим заготовку окна с полуфункциональным интерфейсом для продолжения выполнения
упражнения
Следующим шагом мы создадим класс с событием, Button, и
упакуем его в отдельный файл с именем MyButton.cs. Модель расширения кнопки
будет удобна для возбуждения нашего события по перекрытому виртуальному методу OnClick().
Добавьте к текущему
проекту командой Project/Add New Item новый файл с именем MyButton.cs по
шаблону Custom Control (WPF), как показано на рисунке
Удалите сопутствующую
папку Themes вместе с ее содержимым, которую автоматически создала оболочка
для выбранного шаблона
Шаблон Custom Control (WPF) применяется для разработки пользовательских
компонентов 'с нуля', но мы его здесь использовали потому, что в нем наиболее
полно представлены подключенные пространства имен WPF
Заполните файл MyButton.cs следующим кодомusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace UserEvents
{
// Класс определения пользовательского события Tap,
// возбуждаемого по щелчку мыши на расширенной кнопке
public class MyButton : Button
{
// Объявляем базовое поле события
public static readonly RoutedEvent TapEvent;
// Инициализируем в статическом конструкторе базовое поле события
// (можно было инициализировать сразу при объявлении поля, без конструктора)
static MyButton()
{
TapEvent = EventManager.RegisterRoutedEvent(
"Tap", // Зарегистрированное имя
RoutingStrategy.Bubble, // Стратегия перенаправления
typeof(RoutedEventHandler), // Тип делегата обработчиков
typeof(MyButton) // Тип владельца события
);
}
// Контейнер для создания обработчиков в XAML
public event RoutedEventHandler Tap
{
add { base.AddHandler(TapEvent, value); }
remove { base.RemoveHandler(TapEvent, value); }
}
}
}
Событие нужно чем-то возбудить, собственно для этого мы и выбрали расширение
именно кнопки. Переопределяемые события возбуждаются методом RaiseEvent(),
который наследуется от класса UIElement всеми элементами управления пользовательского
интерфейса (кроме документных для ContentElement ).
Добавьте в класс-расширение MyButton кнопки перекрытие виртуального метода OnClick(), наследуемого от
базового класса Button, со следующим кодомpublic static int count; // Счетчик перехвата события
// Перекроем метод щелчка базовой кнопки для возбуждения события
protected override void OnClick()
{
count = 0; // Сбрасываем счетчик
//Console.Clear(); // Очищаем консоль
base.RaiseEvent(new RoutedEventArgs(MyButton.TapEvent));// Возбуждаем событие
}
Необязательный поле-счетчик count мы ввели для нумерации узлов прохождения
прослушиваемого события при выдачи результатов. Он должен накапливать значение,
поэтому объявлен как статический, чтобы иметь область видимости уровня объекта-типа,
а не объекта-экземпляра.
Далее мы вложим пользовательское событие в элементы логического дерева интерфейса
и прикрепим к нему обработчики для прослушивания. Заметим, что владельцем
является сам класс-расширение кнопки, поэтому
обработчик щелчка нужно прикрепить прямо к событию.
Для наглядности
часть обработчиков Window1 (или индивидуально в каждый прослушивающий элемент)
нужно добавить пространство имен по синтаксису
xmlns:custom="clr-namespace:UserEvents"
с любым уникальным именем, например, custom.
Дополните разметку
следующим выделенным кодом<Window x:Class="UserEvents.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Пользовательские Routed-события" Height="300" Width="300"
Background="Red"
ToolTip="Элемент Window - Red"
MinWidth="300"
MinHeight="300"
ResizeMode="CanResizeWithGrip"
xmlns:custom="clr-namespace:UserEvents"
Name="nWindow1"
>
<DockPanel
Name="nDockPanel"
>
<Grid
Width="220"
Height="200"
Background="Green"
ToolTip="Элемент Grid - Green"
custom:MyButton.Tap="Grid_Tap"
>
<UniformGrid
Height="140" Width="130"
Background="Blue"
ToolTip="Элемент UniformGrid - Blue"
custom:MyButton.Tap="UniformGrid_Tap"
>
<custom:MyButton
VerticalAlignment="Center"
ToolTip="Элемент Button - #FFD4D0C8"
Margin="5,0,5,0"
Tap="MyButton_Tap"
>
Возбудить событие
</custom:MyButton>
</UniformGrid>
</Grid>
</DockPanel>
</Window>
Элементам Window1 и DockPanel мы только присвоили имена. Автоматически создать
для них заготовки обработчиков с нужной сигнатурой оболочка не сможет, поскольку
она их не видит. А для элементов с вложенными событиями - создаст.
Щелкайте на записях
присоединения обработчиков в элементах Grid, UniformGrid, MyButton кода разметки
и командой контекстного меню Navigate to Event Handler создайте
в процедурном коде соответствующие заготовки обработчиков
Создайте в процедурном
коде по любой из полученных заготовок еще два обработчика с именами nDockPanel_Tap() и nWindow1_Tap() и той же сигнатуройВ результате должны быть созданы обработчики с именами:
Добавьте в экземплярный
конструктор класса MyButton процедурный код динамического прикрепления
обработчиков к nDockPanel и nWindow1 следующим образом// Конструктор экземпляра
public Window1()
{
InitializeComponent();
// Динамический способ присоединения обработчиков
nWindow1.AddHandler(MyButton.TapEvent,
new RoutedEventHandler(this.nWindow1_Tap));
nDockPanel.AddHandler(MyButton.TapEvent,
new RoutedEventHandler(this.nDockPanel_Tap));
}
Добавьте в конец класса Window1 функцию ShowTap() вывода результатов прослушивания события Tap void ShowTap(object obj, RoutedEventArgs args)
{
if (MyButton.count == 0)
{
System.Diagnostics.Debug.WriteLine(
String.Format("\n\t Стратегия маршрутизации: {0}",
args.RoutedEvent.RoutingStrategy));
}
String typeName = obj.GetType().Name;
System.Diagnostics.Debug.WriteLine(
String.Format("{0}) {1}: Наблюдаю событие Tap",
++MyButton.count, typeName));
}
Данная функция будет выводить информацию о прохождении события по элементам
дерева в панель оболочки Output. Применение метода String. Format() объясняется
тем, что ни одна из 4-х перегрузок функции Debug. WriteLine() не способна принимать
строку форматирования со спецификаторами формата типа {0}, {1}, и т.д. (в отличие
от метода Console. WriteLine() ). При каждом новом возбуждении события в кнопке-источнике
мы будем добавлять
к выводу результатов заголовок с применяемой стратегией маршрутизации.
Если
у вас панель Output еще не включена, то включите ее командой меню оболочки View/OutputРезультаты можно выводить и на консольное окно. Тогда нужно выполнить следующее:
ShowTap() заменить метод
System.Diagnostics.Debug. WriteLine() на Console. WriteLine()Windows Application на Console Application командой Project/Properties (в списке Output type )
Вставьте в каждый из
созданных обработчиков Tap вызов функции ShowTap() по следующему образцуprivate void nWindow1_Tap(object sender, RoutedEventArgs e)
{
this.ShowTap(sender, e);
}
Здесь мы стремились сделать код как можно унифицированнее, чтобы в случае
смены целевого объекта вывода результатов, например, с панели Output на
консоль, не пришлось бы много исправлять.
Наконец-то наступил момент истины - время испытать наш код и подтвердить правильность механизма работы пользовательских событий, который должен быть точно таким же, как и библиотечных.
Запустите последовательно
приложение с разными значениям стратегии маршрутизации (перечисление RoutingStrategy )
при регистрации события в классе MyButton. В окне Output должен появится
следующий результат| Стратегия | Результат |
|---|---|
| RoutingStrategy.Bubble | Стратегия маршрутизации: Bubble |
| RoutingStrategy.Tunnel | Стратегия маршрутизации: Tunnel |
| RoutingStrategy.Direct | Стратегия маршрутизации: Direct |
В таблице наглядно показано, как осуществляется стратегия маршрутизации события
по дереву элементов: событие всплывает, нисходит или обрабатывается в месте
возбуждения и немедленно останавливается ( Bubble, Tunnel или Direct ).
Иногда может понадобиться такой обработчик
Для одного события можно создать несколько обработчиков уровня класса, по одному для каждого дочернего типа. Регистрация обработчиков должна выполняться только в коде, а именно в статическом конструкторе класса окна. Статический конструктор отличается тем, что он не должен иметь перегрузок, не принимает аргументы (конструктор по умолчанию) и вызывается сразу, как только объект-тип будет загружен в память (не дожидаясь создания хотя бы одного объекта-экземпляра).
Посмотрим это на простом примере, нам важен сам код регистрации обработчика уровня класса.
Добавьте в класс окна Window1 следующий код (теперь листинг файла Window1.xaml.cs приводится полностью)using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace UserEvents
{
public partial class Window1 : Window
{
/*
static Window1()
{
// Добавить владельца события ?????
MyButton.TapEvent.AddOwner(typeof(FrameworkElement));
}
*/
// Статический конструктор (без аргументов и перегрузок)
static Window1()
{
// Зарегистрировать обработчик уровня любого класса дерева, который
// сработает перед всеми обработчиками экземпляра этого класса
EventManager.RegisterClassHandler(
typeof(Window1),
MyButton.TapEvent,
new RoutedEventHandler(SuperMethod1));
EventManager.RegisterClassHandler(
typeof(System.Windows.Controls.Primitives.UniformGrid),
MyButton.TapEvent,
new RoutedEventHandler(SuperMethod2));
}
// Обработчик уровня класса
static void SuperMethod1(object sender, RoutedEventArgs e)
{
String typeName = sender.GetType().Name;
System.Diagnostics.Debug.WriteLine(
String.Format("{0}) {1}: Суперобработчик события Tab",
++MyButton.count, typeName));
}
// Обработчик уровня класса
static void SuperMethod2(object sender, RoutedEventArgs e)
{
String typeName = sender.GetType().Name;
System.Diagnostics.Debug.WriteLine(
String.Format("{0}) {1}: Суперобработчик события Tab",
++MyButton.count, typeName));
}
// Конструктор экземпляра
public Window1()
{
InitializeComponent();
// Динамический способ присоединения обработчиков
nWindow1.AddHandler(MyButton.TapEvent,
new RoutedEventHandler(this.nWindow1_Tap));
nDockPanel.AddHandler(MyButton.TapEvent,
new RoutedEventHandler(this.nDockPanel_Tap));
}
private void MyButton_Tap(object sender, RoutedEventArgs e)
{
this.ShowTap(sender, e);
}
private void UniformGrid_Tap(object sender, RoutedEventArgs e)
{
this.ShowTap(sender, e);
}
private void Grid_Tap(object sender, RoutedEventArgs e)
{
this.ShowTap(sender, e);
}
private void nDockPanel_Tap(object sender, RoutedEventArgs e)
{
this.ShowTap(sender, e);
}
private void nWindow1_Tap(object sender, RoutedEventArgs e)
{
this.ShowTap(sender, e);
}
void ShowTap(object obj, RoutedEventArgs args)
{
if (MyButton.count == 0)
{
System.Diagnostics.Debug.WriteLine(
String.Format("\n\t Стратегия маршрутизации: {0}",
args.RoutedEvent.RoutingStrategy));
}
String typeName = obj.GetType().Name;
System.Diagnostics.Debug.WriteLine(
String.Format("{0}) {1}: Наблюдаю событие Tap",
++MyButton.count, typeName));
}
}
}
Закомментированный код оставлен для размышлений по поводу метода AddOwner().
Как и где его применять, я так и не смог разобраться по MSDN.
Запустите приложение,
результат будет таким (для стратегии Bubble )| Стратегия маршрутизации: Bubble |
Объект RoutedEventArgs, который мы передали событию при возбуждении в
перекрытом методе OnClick() несет в себе только ту информацию, которую предусмотрели
разработчики библиотеки. Мы можем добавить свою информацию к нашему события,
если расширим класс RoutedEventArgs.
Поскольку стандартный делегат
public delegate void RoutedEventHandler(object sender, RoutedEventArgs e)
задает сигнатуру обработчиков такую, что второй аргумент ожидает ссылку на
объект RoutedEventArgs, то ссылку на более толстый объект-расширение обработчик
события примет спокойно. Но нужно иметь ввиду, для того, чтобы добраться до
членов расширения переданного в обработчик объекта, необходимо будет повысить
полномочия ссылки до нашего толстого объекта.
Добавьте в самый конец
файла MyButton.cs после всего существующего кода следующее расширение
библиотечного класса RoutedEventArgs объекта-аргумента событияnamespace UserEvents
{
// Расширяем класс аргументов
class MyEventArgs : RoutedEventArgs
{
// Соблюдаем сигнатуру родителя
public MyEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
{
}
string message;// Базовое поле свойства
public string Message // Свойство чтения/записи
{
get { return message; }
set { message = value; }
}
}
}
Здесь в расширение мы скромно добавили только одно свойство вместе с базовым полем хранения данных. Но при необходимости можно добавить любое количество членов расширения.
Модифицируйте перекрытый
метод OnClick() так, чтобы в возбуждаемое событие передавался объект-расширение
с начальным значением нового свойства// Перекроем метод щелчка базовой кнопки для возбуждения события
protected override void OnClick()
{
count = 0; // Сбрасываем счетчик
//Console.Clear(); // Очищаем консоль
//base.RaiseEvent(new RoutedEventArgs(MyButton.TapEvent));// Возбуждаем событие
// Добавляем информацию и отсылаем
MyEventArgs myEventArgs = new MyEventArgs(MyButton.TapEvent);
myEventArgs.Message = "Привет студентам от Снеткова!";
base.RaiseEvent(myEventArgs);// Возбуждаем событие
}
Модифицируйте в файле Window1.xaml.cs вспомогательную функцию ShowTap() следующим образомvoid ShowTap(object obj, RoutedEventArgs args)
{
if (MyButton.count == 0)
{
System.Diagnostics.Debug.WriteLine(
String.Format("\n\t Стратегия маршрутизации: {0}",
args.RoutedEvent.RoutingStrategy));
}
String typeName = obj.GetType().Name;
/*
System.Diagnostics.Debug.WriteLine(
String.Format("{0}) {1}: Наблюдаю событие Tap",
++MyButton.count, typeName));
*/
// Повышаем полномочия ссылки и извлекаем сообщение
MyEventArgs e = args as MyEventArgs;
string message = e.Message;
// Выводим информацию
System.Diagnostics.Debug.WriteLine(
String.Format("{0}) {1}: Наблюдаю событие Tap.\n"
+ "\tПолучил сообщение: {2}",
++MyButton.count, typeName, message));
// Изменяем информацию в объекте-аргументе
e.Message = String.Format("Привет студентам от {0} и {1}!",
typeName, args.RoutedEvent.OwnerType.Name);
}
В каждом обработчике мы намеренно меняем значение свойства Message объекта-аргумента
события на новое, чтобы подчеркнуть такую возможность. Один и тот же объект
аргумента движется вместе с событием и последовательно попадает в подписанные
на событие обработчики элементов, где и может быть изменен. Для получения доступа
к свойству Message нашего объекта-аргумента предварительно нужно повысить полномочия
ссылки базового типа RoutedEventArgs. Вспомним также, что в любом обработчике
мы можем остановить дальнейшее продвижение события, присвоив свойству объекта-аргумента
args. Handled значение true.
Запустите приложение,
должен получиться такой результатСтратегия маршрутизации: Bubble
1) MyButton: Наблюдаю событие Tap.
Получил сообщение: Привет студентам от Снеткова!
2) UniformGrid: Суперобработчик события Tab
3) UniformGrid: Наблюдаю событие Tap.
Получил сообщение: Привет студентам от MyButton и MyButton!
4) Grid: Наблюдаю событие Tap.
Получил сообщение: Привет студентам от UniformGrid и MyButton!
5) DockPanel: Наблюдаю событие Tap.
Получил сообщение: Привет студентам от Grid и MyButton!
6) Window1: Суперобработчик события Tab
7) Window1: Наблюдаю событие Tap.
Получил сообщение: Привет студентам от DockPanel и MyButton!
Старая добрая парадигма программирования для взаимодействия приложения и пользователя состоит в генерации событий и их обработки. События сигнализируют о возникновении определенных обстоятельств, требующих реакции приложения: будь то действия пользователя, возникновение критических ситуаций или определенных условий. События возбуждаются источниками и перехватываются обработчиками (если они есть), которые и выполняют закрепленные за ними действия в ответ на эти события.
События являются удобным инструментом программирования, делающим код более прозрачным и естественным. События - это низкий уровень модели программирования. Их применение в программировании пользовательского интерфейса, когда управление приложением на уровне задач может осуществляться через несколько разных инструментов, требующих одинаковой реакции, иногда становится громоздким. Может потребоваться не только создание нескольких обработчиков с дублирующим кодом, но и необходимость следить за состоянием сразу нескольких интерфейсных элементов, инициирующих задачу, когда действие выполнить невозможно.
Прямую реакцию приложения на определенные действия пользователя обычно называют задачами, а сами запросы на действия - командами. В WPF разработан новый механизм,
существенно упрощающий связывание задач, запросов и действий, который называется
командами. Команды, как и события, в WPF являются маршрутизованными (Routed
- направленный). Но здесь есть отличие: события могут туннелировать и всплывать,
а команды только всплывают (команды бывают только пузырьковые, восходящие,
всплывающие).
На самом деле всплывают не сами команды, а генерируемые ими маршрутизованные
события. Существует всего несколько специальных событий для поддержки команд.
В классе CommandBinding определены события PreviewCanExecute и CanExecute типа CanExecuteRoutedEventHandler, а также события PreviewExecuted и Executed типа ExecutedRoutedEventHandler. В интерфейсе ICommand, наследуемом классом RoutedCommand,
определено еще событие CanExecuteChanged. Эти события проходят по дереву элементов
до тех пор, пока не будет найден прослушивающий элемент с привязкой CommandBinding для конкретной команды. Вот и все события команд.
Команда включает в себя несколько ингредиентов:
WPF начинает искать привязку.Объекты команд порождаются классом RoutedCommand, который единственный в WPF реализует (и наследует, конечно) интерфейс ICommand,
объявленный следующим образом
public interface ICommand
{
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}
Реализующий класс RoutedCommand расширяет сигнатуру объявленных
в ICommand методов до второго аргумента target
public bool CanExecute(object parameter, System.Windows.IInputElement target) public void Execute(object parameter, System.Windows.IInputElement target)
где - аргумент parameter содержит сведения о команде, а аргумент target -
сведения о целевом элементе команды.
При возбуждении элемента-источника, к которому прикреплена команда (объект RoutedCommand ), вызывается
метод CanExecute(). Этот метод возбуждает туннельное событие PreviewCanExecute, которое начинает спускаться от корня визуального дерева
элементов к источнику команды и собирает информацию с целевых элементов о возможности
выполнения команды. Получив результаты опроса от события PreviewCanExecute метод CanExecute() возбуждает парное восходящее событие CanExecute с заданием
оповестить целевые элементы принять состояние в зависимости от полученной информации
от события PreviewCanExecute, а сам возвращает флаг возможности или невозможности
выполнения команды.
Если метод CanExecute() вернул значение true, значит исполнение команды
возможно и следом вызывается метод Execute(), который последовательно возбуждает
парные события PreviewExecuted и Executed как указание выполнить команду во
всех целевых элементах прикрепленными к ним обработчиками. Событие отмены выполнения CanExecuteChanged вызывается, если диспетчер команд, в котором централизованно
выполняются командные операции, обнаруживает в источнике команды изменения,
которые могут сделать недействительной команду, вызванную, но еще не выполненную
в целевом элементе.
Библиотека .NET Framework имеет множество готовых команд, наиболее часто встречающихся
в приложениях. Команды представлены в пространствах имен System.Windows. Input и System.Windows. Documents следующими статическими классами:
В каждом из приведенных классов команда объявлена как статическое
свойство только для чтения типа RoutedUICommand (или RoutedCommand ), следовательно
при использовании команды создавать экземпляр класса команды не нужно. Команды
имеют имена, ассоциируемые с выполняемой задачей, например, Cut, Copy, New и т.д.
Чтобы команда работала, ее нужно присоединить к свойству Command источника
команды. Источников, к которым присоединена одна и та же команда, может быть
несколько. Элементы, к которым можно присоединить команду, должны наследовать
интерфейс ICommandSource. Только такие элементы будут иметь свойство Command.
Этих элементов в WPF немного и все они показаны на рисунке

Интерфейс ICommandSource реализует свои свойства в ближайших наследниках ButtonBase, Hyperlink, InputBinding, MenuItem по общему синтаксису:
public System.Windows.Input.ICommand Command { get; set; }
public object CommandParameter { get; set; }
public System.Windows.IInputElement CommandTarget { get; set; }
В источнике для присоединения команды достаточно присвоить значение только
ссылке Command на объект команды (на экземпляр RoutedCommand или RoutedUICommand ). Остальные свойства являются необязательными и служат для
уточнения команды:
CommandParameter - тип данных, определяемый пользователем, который используется
для передачи информации обработчикам, реализующим командуCommandTarget - идентифицирует элемент, в котором должна выполняться данная
команда. Если значение свойства CommandTarget не задано, в качестве цели
команды используется элемент, в котором установлен фокус вводаОбратите внимание, что два наследуемые источниками команд свойства-ссылки Command и CommandTarget имеют типы интерфейсов, что позволяет адресовать любые
команды и целевые элементы, наследующие эти интерфейсы.
Вот, например, как можно присоединить команду Paste к элементу меню в разметке
<StackPanel>
<Menu Header="Edit">
<MenuItem Command="ApplicationCommands.Paste" />
</Menu>
<TextBox />
</StackPanel>
Для библиотечных команд синтаксис разметки допускает использование сокращенной записи, например
<StackPanel>
<Menu Header="Edit">
<MenuItem Command="Paste" />
</Menu>
<TextBox />
</StackPanel>
Теперь, если объект TextBox имеет фокус ввода и буфер обмена содержит текст,
то при выборе пользователем этого пункта меню текст из буфера будет вставлен
в элемент TextBox. Обратите
внимание, что объект MenuItem не устанавливает свойство Header для отображения
в нем названия команды Paste. Если свойство для текста названия команды в источнике
опущено, то источник способен сам извлекать эту информацию из объекта присоединенной
команды.
А вот альтернативный вариант присоединения команды Paste к элементу меню
в процедурном коде
// Создание объектов пользовательского интерфейса StackPanel mainStackPanel = new StackPanel(); TextBox pasteTextBox = new TextBox(); Menu stackPanelMenu = new Menu(); MenuItem pasteMenuItem = new MenuItem(); // Добавление объектов в коллекции панели и меню stackPanelMenu.Items.Add(pasteMenuItem); mainStackPanel.Children.Add(stackPanelMenu); mainStackPanel.Children.Add(pasteTextBox); // Присоединение библиотечной команды Paste к источнику команд pasteMenuItem.Command = ApplicationCommands.Paste;
Аналогичный пример можно привести для присоединения команды к контекстному меню.
<StackPanel>
<StackPanel.ContextMenu>
<ContextMenu>
<MenuItem Command="ApplicationCommands.Paste" />
</ContextMenu>
</StackPanel.ContextMenu>
</StackPanel>
// Создание объектов пользовательского интерфейса StackPanel cmdSourcePanel = new StackPanel(); ContextMenu cmdSourceContextMenu = new ContextMenu(); MenuItem cmdSourceMenuItem = new MenuItem(); // Добавление ContextMenu в StackPanel cmdSourcePanel.ContextMenu = cmdSourceContextMenu; cmdSourcePanel.ContextMenu.Items.Add(cmdSourceMenuItem); // Присоединение библиотечной команды Paste к источнику команд cmdSourceMenuItem.Command = ApplicationCommands.Paste;
Как только мы присоединим команду к элементу-источнику, он сразу же становится
недоступен, как будто бы его свойство IsEnabled приобрело значение false. Это
происходит потому, что мы не привязали команду к прослушивающему ее элементу.
Как же это сделать?
Любой элемент управления WPF наследует от класса System.Windows. UIElement,
который имеет свойство-коллекцию CommandBindings типа CommandBindingCollection,
предназначенную для того, чтобы можно было сделать этот элемент прослушивающим
команды. В эту коллекцию помещаются специальные объекты привязки CommandBinding,
у которых имеется свойство Command и событие Executed. Мы создаем объекты CommandBinding,
присоединяем к их свойству Command нужные объекты команд, а к событию Executed - соответствующие обработчики. Затем эти настроенные объекты добавляем
в коллекцию CommandBindings элемента. Так элемент становится прослушивающим.
Как уже говорилось, когда источник команды возбуждает присоединенный к нему
объект команды, тот последовательно выполняет методы CanExecute() и Execute() интерфейса ICommand. Эти методы генерируют командные события, которые
движутся по дереву элементов, проверяя их коллекции CommandBindings с целью
обнаружить объект CommandBinding, соответствующий возбужденной команде. Как
только такой объект привязки будет обнаружен в прослушивающем команду элементе,
немедленно выполнится подписанный на событие Executed обработчик.
В качестве прослушивающего можно сделать любой элемент
логического дерева. Но для получения наибольшей гибкости привязки команд рекомендуется
добавлять в корневой элемент - окно (или контейнер элемента Page ).
Построим простое приложение, в котором присоединим библиотечные команды ApplicationCommands. Open и ApplicationCommands. Save, каждую - к двум элементам-источникам: кнопке и
главному меню окна. Дополнительным источником команд будет уже встроенная
в них клавиатурная комбинация Ctrl+O и Ctrl+S. В качестве прослушивающего элемента
выберем окно. В коллекцию CommandBindings окна добавим два объекта привязки CommandBinding, связывающие между собой команду и обработчик. Привязку выполним
в разметке XAML.
Добавьте к решению
командой оболочки File/Add/New Project новый проект с именем BindingCommandsXAML и
назначьте его стартовым в панели Solution Explorer командой контекстного
меню Set as StartUp Project
Заполните файл разметки Window1.xaml следующим кодом<Window x:Class="BindingCommandsXAML.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Декларативная привязка" Height="300" Width="300"
Background="#FFD4D0C8"
WindowStartupLocation="CenterScreen"
>
<StackPanel Margin="5">
<Menu>
<MenuItem Header="_File">
<MenuItem Command="ApplicationCommands.Open" />
<MenuItem Command="ApplicationCommands.Save" />
</MenuItem>
</Menu>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Open"
Content="Open"
/>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Save"
Content="Save"
/>
</StackPanel>
</Window>
Мы присоединили к интерфейсным элементам объекты команд и сделали их таким образом источниками этих команд.
Запустите проект и
убедитесь, что источники команд не получают фокус ввода и являются недоступнымиНесмотря на то, что в элементах меню нами не были заданы атрибуты Header,
команды сами прописали нужные названия команд и добавили подсказки для горячих
клавиш.
Добавьте в разметку
объекты привязки следующим образом<Window x:Class="BindingCommandsXAML.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Декларативная привязка" Height="300" Width="300"
Background="#FFD4D0C8"
WindowStartupLocation="CenterScreen"
>
<Window.CommandBindings>
<CommandBinding
Command="ApplicationCommands.Open"
Executed="OpenCommand_Executed">
</CommandBinding>
<CommandBinding
Command="ApplicationCommands.Save"
Executed="SaveCommand_Executed">
</CommandBinding>
</Window.CommandBindings>
<StackPanel Margin="5">
<Menu>
<MenuItem Header="_File">
<MenuItem Command="ApplicationCommands.Open" />
<MenuItem Command="ApplicationCommands.Save" />
</MenuItem>
</Menu>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Open"
Content="Open"
/>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Save"
Content="Save"
/>
</StackPanel>
</Window>
Вызовите контекстное
меню для записей привязки обработчиков и командой Navigate to Event Handler создайте заготовки обработчиков в файле процедурного кода
Запустите приложение
и убедитесь, что источники команд стали доступными, хотя обработчики еще пустые
В меню оболочки командой Project/Add Reference добавьте к текущему проекту ссылку на библиотечную
сборку System.Windows.Forms.dllМы уже обсуждали ранее, что нельзя напрямую размещать в окне WPF интерфейсные
элементы Windows Forms, для этого предназначен переходной контейнер <WindowsFormsHost>.
Но в нашем случае мы будем открывать самостоятельные диалоговые окна Windows Forms и не собираемся смешивать в одном окне интерфейсные элементы из разных технологий.
Дополните в процедурном
файле Window1.xaml.cs класс Window1 следующим кодомpublic partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
// Обработчик команды Open
System.Windows.Forms.OpenFileDialog openFileDialog = null;
private void OpenCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
if (openFileDialog == null)
openFileDialog = new System.Windows.Forms.OpenFileDialog();
openFileDialog.ShowDialog();
this.Focus();
}
// Обработчик команды Save
System.Windows.Forms.SaveFileDialog saveFileDialog = null;
private void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
if (saveFileDialog == null)
saveFileDialog = new System.Windows.Forms.SaveFileDialog();
saveFileDialog.ShowDialog();
this.Focus();
}
}
Запустите приложение
и испытайте возможность запуска каждой команды через любой из трех источников:
кнопку, меню или клавиатурную комбинациюРекомендуется при совместном использовании библиотек WPF и Windows Forms не подключать пространство имен System.Windows.Forms в
инструкции using, а использовать полное имя требуемых классов Windows Forms,
что мы и сделали. Это нужно для того, чтобы избежать конфликта имен, поскольку
в библиотеках разных технологий встречаются типы с одинаковыми именами, например, Color, Pen, Brush.
Для тренировки, перекройте
в классе Window1 функцию диспетчеризации OnKeyDown() клавиатурного события KeyDown. Код начните вводить с ключевого слова override, чтобы воспользоваться
услугами подсказчика кода IntelliSense
Заполните созданную
оболочкой заготовку кодом, обеспечивающим закрытие окна приложения по нажатию
клавиши Esc// Выход по клавише Esc с предупреждением
protected override void OnKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Escape:
MessageBoxResult result =
MessageBox.Show("Закрыть приложение ?", "",
MessageBoxButton.OKCancel,
MessageBoxImage.Question);
if (result == MessageBoxResult.OK)
this.Close();
break;
}
}
Запустите приложение,
раскройте мышью меню File и нажимайте на клавишу EscМы видим, что клавиша Esc действует только на те элементы, которым на момент нажатия передан
фокус ввода. Но если бы мы перекрыли функцию OnPreviewKeyDown(), то окно
приложения закрывалось бы сразу. Для проверки этого можно в коде просто поменять
имя перекрытой функции с OnKeyDown на OnPreviewKeyDown, так как обе функции
имеют одинаковую сигнатуру.
Для сравнения можно привести код использования равнозначного по функциональности
класса диалогового окна из библиотеки System.Windows.Forms.dll
protected override void OnKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Escape:
System.Windows.Forms.DialogResult result =
System.Windows.Forms.MessageBox.Show("Закрыть приложение ?", "",
System.Windows.Forms.MessageBoxButtons.OKCancel,
System.Windows.Forms.MessageBoxIcon.Question);
if (result == System.Windows.Forms.DialogResult.OK)
this.Close();
break;
}
}
Коды немного различаются, причем последний работает похуже - после его закрытия
окно WPF теряет фокус ввода. Но мы немного отвлеклись от темы команд WPF, продолжим...
Названия кнопок в разметке мы жестко подписали, но существует другой способ,
когда подпись можно извлечь из самой команды (если она есть). Если объект команды
произведен от класса RoutedCommand, то там нет свойства Text, которое можно
было бы использовать. Но если команда имеет тип RoutedUICommand, то этот класс
добавляет такое свойство и оно может иметь значение.
Перепишите элементы
кнопок в разметке файла Window1.xaml следующим образом<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Open"
Content="{x:Static ApplicationCommands.Open}"
/>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Save"
Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"
/>
Запустите приложение
- сейчас надписи на кнопках должны выглядеть так
Команды WPF можно вызывать и напрямую, а необязательно присоединять к источникам.
Правда, это будет уже извращение и сам механизм команд будет использоваться
только частично. При таком подходе придется реагировать на состояния источников
и на возможность выполнения команды самостоятельным кодом. Но знать о существовании
такой возможности программисту следует. Покажем это...
Добавьте к интерфейсу
окна еще четыре кнопки так<Window x:Class="BindingCommandsXAML.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Декларативная привязка" Height="300" Width="300"
Background="#FFD4D0C8"
WindowStartupLocation="CenterScreen"
>
<Window.CommandBindings>
<CommandBinding
Command="ApplicationCommands.Open"
Executed="OpenCommand_Executed">
</CommandBinding>
<CommandBinding
Command="ApplicationCommands.Save"
Executed="SaveCommand_Executed">
</CommandBinding>
</Window.CommandBindings>
<StackPanel Margin="5">
<Menu>
<MenuItem Header="_File">
<MenuItem Command="ApplicationCommands.Open" />
<MenuItem Command="ApplicationCommands.Save" />
</MenuItem>
</Menu>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Open"
Content="{x:Static ApplicationCommands.Open}"
/>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Save"
Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"
/>
<Button Margin="5" Padding="5" Focusable="False"
Content="DirectCommandsOpen"
Click="directCommandsOpen_Click"
/>
<Button Margin="5" Padding="5" Focusable="False"
Content="DirectCommandsSave"
Click="directCommandsSave_Click"
/>
<Button Margin="5" Padding="5" Focusable="False"
Content="DirectBindingsOpen"
Click="directBindingsOpen_Click"
/>
<Button Margin="5" Padding="5" Focusable="False"
Content="DirectBindingsSave"
Click="directBindingsSave_Click"
/>
</StackPanel>
</Window>
Вызовите контекстное
меню для записей события Click и командой Navigate to Event Handler создайте четыре обработчика в файле процедурного кода
Заполните обработчики
следующим кодом прямого вызова командprivate void directCommandsOpen_Click(object sender, RoutedEventArgs e)
{
ApplicationCommands.Open.Execute(null, this);
}
private void directCommandsSave_Click(object sender, RoutedEventArgs e)
{
ApplicationCommands.Save.Execute(null, this);
}
private void directBindingsOpen_Click(object sender, RoutedEventArgs e)
{
this.CommandBindings[0].Command.Execute(null);
}
private void directBindingsSave_Click(object sender, RoutedEventArgs e)
{
this.CommandBindings[1].Command.Execute(null);
}
В прямых вызовах двух первых обработчиков начальный параметр метода Execute() ожидает объект с дополнительной информацией, который будет передан на обработчик
команды. Второй параметр принимает целевой элемент привязки команды с возможностями
интерфейса IInputElement, который будет прослушивать команду и вызывать обработчик.
В нашем случае это объект окна. Два следующих обработчика используют вызовы
команд из коллекции прослушивающего элемента (окна), привязанные к нему в разметке.
Запустите приложение
и убедитесь, что добавленные кнопки инициируют прямой вызов командЧастично повторим предыдущее упражнение, но привязку команд выполним в процедурном коде.
Добавьте к решению
командой File/Add/New Project новый проект с именем BindingCommandsCode и назначьте его стартовым
Откорректируйте разметку
окна Window1 так<Window x:Class="BindingCommandsCode.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Кодовая привязка" Height="300" Width="300"
Background="#FFD4D0C8"
WindowStartupLocation="CenterScreen"
>
<StackPanel Margin="5">
<Menu>
<MenuItem Header="_File">
<MenuItem Command="ApplicationCommands.Open" />
<MenuItem Command="ApplicationCommands.Save" />
</MenuItem>
</Menu>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Open"
Content="Open"
/>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Save"
Content="Save"
/>
</StackPanel>
</Window>
Заметьте, что в этой разметке отсутствует как секция привязки команд, так и ссылки на обработчики команд. Все это мы выполним в процедурной части окна.
Внесите следующие изменения
в класс Window1 файла процедурного кода Window1.xaml.cspublic partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
// Регистрация обработчика
this.Loaded += new RoutedEventHandler(Window1_Loaded);
}
void Window1_Loaded(object sender, RoutedEventArgs e)
{
// Привязка команд в коде
CommandBinding binding = new CommandBinding();
binding.Command = ApplicationCommands.Open;
binding.Executed +=
new ExecutedRoutedEventHandler(OpenCommand_Executed);
this.CommandBindings.Add(binding);
binding = new CommandBinding();
binding.Command = ApplicationCommands.Save;
binding.Executed +=
new ExecutedRoutedEventHandler(SaveCommand_Executed);
this.CommandBindings.Add(binding);
}
void OpenCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("Выполнена команда Open");
}
void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("Выполнена команда Save");
}
}
Запустите приложение
и убедитесь в работоспособности механизма команд WPF
Комбинации клавиш, предоставляющие прямой InputGestures, а можно хранить
в прослушивающем элементе в коллекции InputBindings наряду с командой. Кроме
интерфейсных элементов жесты могут служить еще одним источником команд. Команда,
имеющая жесты в своей коллекции, может вообще не быть присоединенной ни к одному
из визуальных элементов, а возбуждаться только жестами.
Жесты представленны абстрактным классом System.Windows.Input. InputGesture.
Этот класс наследует двум типам жестов: клавиатурных и мыши, которые представлены
объектами KeyGesture и MouseGesture соответственно. Команды, в свою очередь,
имеют коллекцию жестов InputGestures, которую заранее можно наполнить объектами
нужных жестов клавиатуры и мыши. Комбинации клавиш и кнопок мыши проще
всего добавлять в объект жеста через его конструктор при создании экземпляра
по схеме ( Key, ModifierKeys ) и ( MouseAction, ModifierKeys ).
В следующем примере показано, как в команду можно добавить жесты, которые будут ее запускать
// Клавиатурный жест Control+F
InputGesture gesture = new KeyGesture(Key.F, ModifierKeys.Control, "Ctrl+F");
ApplicationCommands.Find.InputGestures.Add(gesture);
// Комбинированный жест Control+LeftClick
gesture = new MouseGesture(MouseAction.LeftClick, ModifierKeys.Control);
ApplicationCommands.Find.InputGestures.Add(gesture);
// Клавиатурный жест Control+Q
KeyGesture keyGesture = new KeyGesture(Key.Q, ModifierKeys.Control, "Ctrl+Q");
ApplicationCommands.Find.InputGestures.Add(keyGesture);
// Комбинированный жест Alt+LeftClick
MouseGesture mouseGesture = new MouseGesture();
mouseGesture.MouseAction = MouseAction.LeftClick;
mouseGesture.Modifiers = ModifierKeys.Alt;
ApplicationCommands.Find.InputGestures.Add(mouseGesture);
//gesture = new MouseGesture(MouseAction.LeftClick, ModifierKeys.Alt);
//ApplicationCommands.Find.InputGestures.Add(gesture);
Теперь команда Find будет запускаться щелчком на кнопке, щелчком левой кнопки
мыши с нажатой клавишей Ctrl, комбинациями клавиш Ctrl+F или Ctrl+Q.
Продолжим модификацию нашего упражнения, чтобы проиллюстрировать сказанное о жестах.
Добавьте в обработчик
события Loaded класса окна Window1 следующий код, модифицирующий жесты библиотечных
команд Open и Savepublic partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
// Регистрация обработчика
this.Loaded += new RoutedEventHandler(Window1_Loaded);
}
void Window1_Loaded(object sender, RoutedEventArgs e)
{
// Привязка команд в коде
CommandBinding binding = new CommandBinding();
binding.Command = ApplicationCommands.Open;
binding.Executed +=
new ExecutedRoutedEventHandler(OpenCommand_Executed);
this.CommandBindings.Add(binding);
binding = new CommandBinding();
binding.Command = ApplicationCommands.Save;
binding.Executed +=
new ExecutedRoutedEventHandler(SaveCommand_Executed);
this.CommandBindings.Add(binding);
// Очистка коллекций прежних жестов команд
ApplicationCommands.Open.InputGestures.Clear();
ApplicationCommands.Save.InputGestures.Clear();
// Добавление новых жестов клавиатуры Alt+O и Alt+S
InputGesture key = new KeyGesture(Key.O, ModifierKeys.Alt, "Alt+O");
ApplicationCommands.Open.InputGestures.Add(key);
//
KeyGesture keyGesture = new KeyGesture(Key.S, ModifierKeys.Alt, "Alt+S");
ApplicationCommands.Save.InputGestures.Add(keyGesture);
// Добавление новых жестов мыши Ctrl+LeftClick и Ctrl+RightClick
InputGesture mouse = new MouseGesture(MouseAction.LeftClick, ModifierKeys.Control);
ApplicationCommands.Open.InputGestures.Add(mouse);
//
MouseGesture mouseGesture = new MouseGesture();
mouseGesture.MouseAction = MouseAction.RightClick;
mouseGesture.Modifiers = ModifierKeys.Control;
ApplicationCommands.Save.InputGestures.Add(mouseGesture);
}
void OpenCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("Выполнена команда Open");
}
void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("Выполнена команда Save");
}
}
Вначале мы очистили коллекции команд от прежних жестов, затем добавили свои жесты, в том числе с участием мыши. Коллекции можно было и не очищать, тогда работали бы прежние жесты наряду с вновь добавленными.
Запустите приложение
и убедитесь, что теперь прежние клавиатурные комбинации Ctrl+O и Ctrl+S не
работают, вместо них программа реагирует на жесты клавиатуры Alt+O и Alt+S, а также выполняются комбинированные жесты с участием мыши Ctrl+LeftClick и Ctrl+RightClickВ нашей программе после добавления жестов появился существенный недостаток
- сами жесты мы поменяли, но в меню все-таки остались старые маркеры команд Ctrl+O и Ctrl+S. Хоть мы и пытались в конструкторе KeyGesture() определить в последнем параметре displayString новые маркеры для жестов в меню,
но все осталось по прежнему. Дело здесь в том, что объект меню создается и
инициализируется в конструкторе класса по объекту уже присоединенной к источнику
команды. И это происходит раньше, чем мы в коде начинаем модифицировать саму
команду.
Чтобы это исправить, можно воспользоваться несколькими способами:
Определитете в разметке
в элементах меню свойство InputGestureText следующим образом<Menu>
<MenuItem Header="_File">
<MenuItem Command="ApplicationCommands.Open"
InputGestureText="Alt+O" />
<MenuItem Command="ApplicationCommands.Save"
InputGestureText="Alt+S" />
</MenuItem>
</Menu>
Запустите приложение
и убедитесь, что теперь маркеры команд меню изменилисьВместо разметки изменить свойство InputGestureText можно в процедурном коде, но для этого элементам меню нужно присвоить имена.
Удалите в разметке
из дескрипторов элементов меню параметры Command и InputGestureText, а добавьте
вместо них имена элементов, как показано ниже<Menu>
<MenuItem Header="_File">
<MenuItem Name="openMenu" />
<MenuItem Name="saveMenu" />
</MenuItem>
</Menu>
Добавьте в самый конец
обработчика события Loaded после кода добавления жестов следующую пару строк
динамического присоединения модифицированных команд к меню-источникуvoid Window1_Loaded(object sender, RoutedEventArgs e)
{
// Привязка команд в коде
CommandBinding binding = new CommandBinding();
binding.Command = ApplicationCommands.Open;
binding.Executed +=
new ExecutedRoutedEventHandler(OpenCommand_Executed);
this.CommandBindings.Add(binding);
binding = new CommandBinding();
binding.Command = ApplicationCommands.Save;
binding.Executed +=
new ExecutedRoutedEventHandler(SaveCommand_Executed);
this.CommandBindings.Add(binding);
// Очистка коллекций прежних жестов команд
ApplicationCommands.Open.InputGestures.Clear();
ApplicationCommands.Save.InputGestures.Clear();
// Добавление новых жестов клавиатуры Alt+O и Alt+S
InputGesture key = new KeyGesture(Key.O, ModifierKeys.Alt, "Alt+O");
ApplicationCommands.Open.InputGestures.Add(key);
//
KeyGesture keyGesture = new KeyGesture(Key.S, ModifierKeys.Alt, "Alt+S");
ApplicationCommands.Save.InputGestures.Add(keyGesture);
// Добавление новых жестов мыши Ctrl+LeftClick и Ctrl+RightClick
InputGesture mouse = new MouseGesture(MouseAction.LeftClick, ModifierKeys.Control);
ApplicationCommands.Open.InputGestures.Add(mouse);
//
MouseGesture mouseGesture = new MouseGesture();
mouseGesture.MouseAction = MouseAction.RightClick;
mouseGesture.Modifiers = ModifierKeys.Control;
ApplicationCommands.Save.InputGestures.Add(mouseGesture);
// Присоединяем модифицированные команды к меню-источнику
openMenu.Command = ApplicationCommands.Open;
saveMenu.Command = ApplicationCommands.Save;
}
Запустите приложение
и убедитесь, что теперь маркеры команд меню изменились в соответствии с введенными
нами при формировании новых жестов
Разметку меню сделайте
такой<Menu>
<MenuItem Header="_File">
<MenuItem Name="openMenu"
Command="ApplicationCommands.Open" />
<MenuItem Name="saveMenu"
Command="ApplicationCommands.Save" />
</MenuItem>
</Menu>
Замените в обработчике Window1_Loaded () только что добавленный код на новый// Присоединяем модифицированные команды к меню-источнику
//openMenu.Command = ApplicationCommands.Open;
//saveMenu.Command = ApplicationCommands.Save;
openMenu.InputGestureText = "Alt+O";
saveMenu.InputGestureText = "Alt+S";
Запустите приложение
и убедитесь, что маркеры команд меню стали правильнымиЕсть еще один способ добавления жестов, о котором стоит упомянуть. Ранее
обсуждалось, что все пользовательские элементы WPF наследуют от UIElement или ContentElement, а эти классы, в свою очередь, имеют в качестве свойства коллекцию InputBindings типа InputBindingCollection. Коллекция может быть заполнена классами KeyBinding или MouseBinding, каждый из которых связывает жест клавиатуры (представленный
объектом KeyGesture ) или мыши (представленный объектом MouseGesture ) с командой.
Оба класса наследуют тип InputBinding.
Например, привязать жесты с командой к объекту окна можно одним из следующих способов
<Window x:Class="Tmp.window"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="300">
<Window.InputBindings>
<KeyBinding Key="F"
Modifiers="Control"
Command="ApplicationCommands.Find" />
<MouseBinding MouseAction="Control+LeftClick"
Command="ApplicationCommands.Find" />
<KeyBinding Key="Q"
Modifiers="Control"
Command="ApplicationCommands.Find" />
<MouseBinding Gesture="Alt+LeftClick"
Command="ApplicationCommands.Find" />
</Window.InputBindings>
<Grid>
<Button Command="ApplicationCommands.Find"
Height="23"
Width="75"
>
Поиск
</Button>
</Grid>
</Window>
В классе MouseBinding свойство Modifiers доступно только для чтения,
поэтому жест для мыши нужно вводить в свойство MouseAction сразу целиком,
как показано в только что приведенном примере.
// Клавиатурный жест Control+F
KeyGesture keyGesture = new KeyGesture(Key.F, ModifierKeys.Control);
KeyBinding keyBinding = new KeyBinding(ApplicationCommands.Find, keyGesture);
this.InputBindings.Add(keyBinding);
// Комбинированный жест Control+LeftClick
MouseGesture mouseGesture = new MouseGesture(MouseAction.LeftClick, ModifierKeys.Control);
MouseBinding mouseBinding = new MouseBinding(ApplicationCommands.Find, mouseGesture);
this.InputBindings.Add(mouseBinding);
// Клавиатурный жест Control+Q
InputGesture gesture = new KeyGesture(Key.Q, ModifierKeys.Control);
ICommand command = ApplicationCommands.Find;
keyBinding = new KeyBinding(command, (KeyGesture)gesture);
this.InputBindings.Add(keyBinding);
// Комбинированный жест Alt+LeftClick
gesture = new MouseGesture();
((MouseGesture)gesture).MouseAction = MouseAction.LeftClick;
((MouseGesture)gesture).Modifiers = ModifierKeys.Alt;
mouseBinding = new MouseBinding();
mouseBinding.Command = command;
mouseBinding.Gesture = gesture;
this.InputBindings.Add(mouseBinding);
В данном упражнении
мы разработаем приложение, частично имитирующее текстовый блокнот, но механизм
команд WPF применять не будем. В последующем упражнении на базе разработанного
кода мы создадим альтернативный вариант текстового блокнота, но уже с
использованием команд. Ух-х-х, поехали!
Командой Add/New Project добавьте к решению новый проект WPF Application с именем Notepad1 и назначьте его стартовымОсновную часть интерфейса приложения, как и положено, мы создадим декларативно
в разметке XAML, а управление - в части процедурного кода C#.
Отредактируйте интерфейсную
часть окна следующим образом<Window x:Class="Notepad1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
>
<DockPanel>
</DockPanel>
</Window>
Добавьте в дескриптор <DockPanel> </DockPanel> разметку создания меню<DockPanel LastChildFill="True">
<!-- Меню -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_New" InputGestureText="Ctrl+N">
</MenuItem>
<MenuItem Header="_Open..." InputGestureText="Ctrl+O" />
<MenuItem Header="_Save" InputGestureText="Ctrl+S" />
<MenuItem Header="Save _As..." />
<Separator />
<MenuItem Header="Page Set_up..." />
<MenuItem Header="P_rint Preview" InputGestureText="Ctrl+F2" />
<MenuItem Header="_Print..." InputGestureText="Ctrl+P" />
<Separator />
<MenuItem Header="E_xit" />
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Header="_Undo" InputGestureText="Ctrl+Z" />
<MenuItem Header="_Redo" InputGestureText="Ctrl+Y" />
<Separator></Separator>
<MenuItem Header="Cu_t" InputGestureText="Ctrl+X" />
<MenuItem Header="_Copy" InputGestureText="Ctrl+C" />
<MenuItem Header="_Paste" InputGestureText="Ctrl+V" />
<MenuItem Header="De_lete" InputGestureText="Del" />
<Separator></Separator>
<MenuItem Header="_Find..." InputGestureText="Ctrl+F" />
<MenuItem Header="Find _Next" InputGestureText="F3" />
<MenuItem Header="_Replace..." InputGestureText="Ctrl+H" />
<MenuItem Header="_Go To..." InputGestureText="Ctrl+G" />
<Separator></Separator>
<MenuItem Header="Select _All" InputGestureText="Ctrl+A" />
</MenuItem>
<MenuItem Header="F_ormat">
<MenuItem Header="_Font..." />
<Separator />
<MenuItem Header="_Word Wrap" IsCheckable="True" IsChecked="True" InputGestureText="Ctrl+W" />
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Header="_About" />
</MenuItem>
</Menu>
</DockPanel>
Знаки подчеркивания мы использовали для того, чтобы выделить символы, по которым
пользователь может раскрывать меню и запускать команды после нажатия клавиш Alt или F10. Отмеченные символы
должны быть уникальными как на горизонтальной линейке на уровне разделов, так
и в рамках одного раздела меню. Каждому пункту присвоено название команды и
клавиатурного жеста. В команде Word Wrap свойство IsCheckable обеспечивает
самопереключаемость флажка, а его начальное состояние задается свойством IsChecked.
Теперь добавим к пунктам меню иконки, которые можно взять в архиве VS2008ImageLibrary.zip,
устанавливаемом на компьютер вместе с VS-2008 и находящимся в каталоге Program Files\Microsoft Visual Studio 9.0\Common7\VS2008ImageLibrary\1033.
Мы будем использовать иконки из папки архива VS2008ImageLibrary\Actions\24bitcolor bitmaps.
Для удобства они скопированы в каталог Source данной работы.
В панели Solution
Explorer вызовите контекстное меню для корня проекта Notepad1 и добавьте командой Add/New Folder новую
папку с именем Images
Вызовите для папки Images контекстное меню и командой Add/Existing Item скопируйте в нее
иконки с расширением .bmp из папки Source:
Подключите иконку NewDocument.bmp к
пункту меню File/New, модифицировав разметку для этого элемента такБыло <MenuItem Header="_New" InputGestureText="Ctrl+N">
</MenuItem>
Стало <MenuItem Header="_New" InputGestureText="Ctrl+N">
<MenuItem.Icon>
<Image Source="Images/NewDocument.bmp" Width="16" Height="16" />
</MenuItem.Icon>
</MenuItem>
Запустите приложение
и убедитесь, что иконка появилась слева от названия пункта меню NewЗдесь есть один недостаток, который бросается в глаза - фон иконки имеет цвет Magenta. В Windows Forms фон устраняется установкой параметра ImageTransparentColor в
значение цвета, который система будет считать прозрачным. В WPF для нейтрализации
фона можно применить другой механизм, задействующий параметр OpacityMask, но
пока мы оставим все как есть.
Если в каждый элемент MenuItem мы будем встраивать дескриптор <MenuItem.Icon> с
элементом Image, то разметка сильно разбухнет и станет нечитабельным. Чтобы
сделать код более элегантным, воспользуемся новым механизмом WPF - логическими
ресурсами. Ресурсы хороши тем, что их можно использовать многократно в различных
местах приложения. В нашем приложении одни и те же иконки понадобятся в меню,
панели инструментов и контекстном меню.
Каждый элемент, производный от класса FrameworkElement, включая окно Window,
наследует от FrameworkElement коллекцию Resources типа словаря ресурсов System.Windows. ResourceDictionary. В эту коллекцию
можно помещать описания пар ключ-объект, ссылки на которые потом можно подключать
к элементам управления для встраивания объектов. Такой прием называется расширением
разметки интерфейсных элементов.
Обязательным элементом при объявлении и использовании ресурса является ключ x:Key="идентификатор_ключа", которому можно присваивать произвольное имя.
В пределах одной коллекции ключи должны быть уникальными. Использование ссылки
на ресурс для свойства элемента выполняется по синтаксису
Свойство="{StaticResource идентификатор_ключа}"
или
Свойство="{DynamicResource идентификатор_ключа}"
Синтаксический анализатор разметки XAML присвоит свойству объявленный в ресурсе
объект. Если применяется ссылка на статический ресурс, то объявление ресурса
должно предшествовать его использованию. Встретив ссылку на ресурс анализатор
начинает поиск объекта с заданным ключом вначале в коллекции Resources того
элемента, где расположена ссылка. Не найдя нужную пару анализатор поднимается
на уровень выше и просматривает коллекцию родительского элемента и так далее,
пока не будет найдена соответствующая ссылке пара. Таким образом, ресурсы можно
объявлять в любом элементе, но лучше сосредоточить их в коллекции <Window.Resources> окна
или <Application.Resources> приложения.
Добавьте после открывающего
дескриптора окна объявление логического ресурса, а в самозакрывающемся дескрипторе
элемена меню - его использование по ключу<Window x:Class="Notepad1.Window1"
...............................................
>
<Window.Resources>
<!-- File -->
<Image x:Key="iconNew" Source="Images/NewDocument.bmp" Width="16" Height="16" />
</Window.Resources>
<DockPanel LastChildFill="True">
<!-- Меню -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<!-- Сокращенные варианты подключения иконок с использованием статических ресурсов -->
<MenuItem Header="_New" InputGestureText="Ctrl+N" Icon="{StaticResource iconNew}" />
...............................................
</MenuItem>
...............................................
</Menu>
</DockPanel>
</Window>
Запустите приложение
и убедитесь, что иконка присутствует слева от названия пункта меню New, но
разметка элемента меню стала значительно короче
Расширьте объявление
ресурсов на все иконки<Window.Resources>
<!-- File -->
<Image x:Key="iconNew" Source="Images/NewDocument.bmp" Width="16" Height="16" />
<Image x:Key="iconOpen" Source="Images/Open.bmp" Width="16" Height="16" />
<Image x:Key="iconSave" Source="Images/Save.bmp" Width="16" Height="16" />
<Image x:Key="iconPageSetup" Source="Images/PrintSetup.bmp" Width="16" Height="16" />
<Image x:Key="iconPrintPreview" Source="Images/PrintPreview.bmp" Width="16" Height="16" />
<Image x:Key="iconPrint" Source="Images/Print.bmp" Width="16" Height="16" />
<!-- Edit -->
<Image x:Key="iconUndo" Source="Images/Edit_Undo.bmp" Width="16" Height="16" />
<Image x:Key="iconRedo" Source="Images/Edit_Redo.bmp" Width="16" Height="16" />
<Image x:Key="iconCut" Source="Images/Cut.bmp" Width="16" Height="16" />
<Image x:Key="iconCopy" Source="Images/Copy.bmp" Width="16" Height="16" />
<Image x:Key="iconPaste" Source="Images/Paste.bmp" Width="16" Height="16" />
<Image x:Key="iconDelete" Source="Images/Delete.bmp" Width="16" Height="16" />
<Image x:Key="iconFind" Source="Images/Find.bmp" Width="16" Height="16" />
<Image x:Key="iconFont" Source="Images/Font.bmp" Width="16" Height="16" />
</Window.Resources>
Дополните разметку
создания главного меню ссылками на ресурс объекта рисунка для свойства Icon<!-- Меню -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<!-- Сокращенные варианты подключения иконок с использованием статических ресурсов -->
<MenuItem Header="_New" InputGestureText="Ctrl+N" Icon="{StaticResource iconNew}" />
<MenuItem Header="_Open..." InputGestureText="Ctrl+O" Icon="{StaticResource iconOpen}" />
<MenuItem Header="_Save" InputGestureText="Ctrl+S" Icon="{StaticResource iconSave}" />
<MenuItem Header="Save _As..." />
<Separator />
<MenuItem Header="Page Set_up..." Icon="{StaticResource iconPageSetup}" />
<MenuItem Header="P_rint Preview" InputGestureText="Ctrl+F2" Icon="{StaticResource iconPrintPreview}" />
<MenuItem Header="_Print..." InputGestureText="Ctrl+P" Icon="{StaticResource iconPrint}" />
<Separator />
<MenuItem Header="E_xit" />
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Header="_Undo" InputGestureText="Ctrl+Z" Icon="{StaticResource iconUndo}" />
<MenuItem Header="_Redo" InputGestureText="Ctrl+Y" Icon="{StaticResource iconRedo}" />
<Separator></Separator>
<MenuItem Header="Cu_t" InputGestureText="Ctrl+X" Icon="{StaticResource iconCut}" />
<MenuItem Header="_Copy" InputGestureText="Ctrl+C" Icon="{StaticResource iconCopy}" />
<MenuItem Header="_Paste" InputGestureText="Ctrl+V" Icon="{StaticResource iconPaste}" />
<MenuItem Header="De_lete" InputGestureText="Del" Icon="{StaticResource iconDelete}" />
<Separator></Separator>
<MenuItem Header="_Find..." InputGestureText="Ctrl+F" Icon="{StaticResource iconFind}" />
<MenuItem Header="Find _Next" InputGestureText="F3" />
<MenuItem Header="_Replace..." InputGestureText="Ctrl+H" />
<MenuItem Header="_Go To..." InputGestureText="Ctrl+G" />
<Separator></Separator>
<MenuItem Header="Select _All" InputGestureText="Ctrl+A" />
</MenuItem>
<MenuItem Header="F_ormat">
<MenuItem Header="_Font..." Icon="{StaticResource iconFont}" />
<Separator />
<MenuItem Header="_Word Wrap" IsCheckable="True" IsChecked="True" InputGestureText="Ctrl+W" />
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Header="_About" />
</MenuItem>
</Menu>
Запустите приложение
и проверьте наличие иконок в элементах главного менюПанель инструментов содержит кнопки быстрого доступа к командам меню, наиболее
часто употребляемые пользователем. Обычно она размещается в верхней части рабочей
области окна после меню. Панель инструментов представляет собой как минимум
один контейнер для элементов управления, чаще всего кнопок, определенный классом ToolBar. Но она может состоять и из нескольких
групп инструментов, где каждая группа пакуется в отдельный контейнер ToolBar.
Для управления совместным стандартным поведением нескольких панелей инструментов
все они упаковываются в контейнер ToolBarTray.
Добавьте в контейнер
размещения DockPanel после разметки меню разметку создания панели инструментов<!-- Панель инструментов -->
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<Button Width="23" Content="{StaticResource iconNew}" />
<Button Width="23" Content="{StaticResource iconOpen}" />
<Button Width="23" Content="{StaticResource iconSave}" />
</ToolBar>
<ToolBar>
<Button Width="23" Content="{StaticResource iconUndo}" />
<Button Width="23" Content="{StaticResource iconRedo}" />
<Separator />
<Button Width="23" Content="{StaticResource iconCut}" />
<Button Width="23" Content="{StaticResource iconCopy}" />
<Button Width="23" Content="{StaticResource iconPaste}" />
<Button Width="23" Content="{StaticResource iconDelete}" />
</ToolBar>
<ToolBar Header="Find:">
<TextBox Width="100" />
<Button Width="23" Content="{StaticResource iconFind}" />
</ToolBar>
</ToolBarTray>
В кнопки панели инструментов мы вставляем иконки из ресурсов по тому же самому ключу, который использовали для вставки в пункты меню. То же самое выполним далее и для контекстного меню.
Добавьте после разметки
панели инструментов разметку создания строки состояния<!-- Строка состояния -->
<StatusBar DockPanel.Dock="Bottom" Height="32" Name="statusBar">
<Label>Simulator Application is Loading</Label>
<Separator />
<ProgressBar Height="20" Width="100" IsIndeterminate="True" />
</StatusBar>
Контейнер StatusBar строки состояния мы привязали к нижней части окна и наполнили
тремя элементами, последний из которых будет имитировать ход процесса загрузки
приложения за счет свойства IsIndeterminate="True".
Добавьте после разметки
строки состояния многострочное текстовое поле редактирования TextBox, который
будет представлять рабочую область окна приложения и занимать все свободное
пространство, поскольку в открывающем дескрипторе панели размещения мы указали
для последнего элемента соответствующий параметр <DockPanel LastChildFill="True"><!-- Многострочное текстовое поле редактирования -->
<TextBox TextWrapping="Wrap"
AcceptsReturn="True"
AcceptsTab="True"
VerticalScrollBarVisibility="Auto"
>
</TextBox>
Параметр TextWrapping="Wrap" переносит строку, если она не помещается по ширине
в текстовое поле. AcceptsReturn="True" включает действие клавиши для переноса
строк (accept -
принимать, признавать). AcceptsTab="True" включает действие клавиши табуляции
в текстовом поле. VerticalScrollBarVisibility="Auto" включает автоматическое
появление линейки скролирования, когда текст выходит за пределы области редактирования
по высоте.
Запустите приложение
и вызовите контекстное меню для элемента редактирования щелчком правой кнопкой
мыши или клавиатурным жестом Shift+F10Мы видим, что элемент редактирования имеет по умолчанию контекстное меню, которое называется встроенным. Некоторые элементы управления имеют встроенное контекстное меню с готовой поддержкой команд.
Проверьте, что команды
меню и клавиатурные комбинации выполняют операции вырезания и копирования
в буфер обмена, а также вставку из буфера. Работает также и встроенная логика
отключения источников команд, когда операция невозможна, например, буфер
обмена пуст и нечего вставлятьДля тренировки, в дальнейшем мы переопределим встроенное
в TextBox контекстное меню и встроенные команды, а заодно и отключим клавиатурные
комбинации, инициирующие встроенные команды.
Добавьте внутрь дескриптора TextBox разметку контекстного меню с подключенными пиктограммами для элементов<TextBox.ContextMenu>
<ContextMenu Width="100">
<MenuItem Header="Cu_t" Icon="{StaticResource iconCut}" />
<MenuItem Header="_Copy" Icon="{StaticResource iconCopy}" />
<MenuItem Header="_Paste" Icon="{StaticResource iconPaste}" />
<MenuItem Header="De_lete" Icon="{StaticResource iconDelete}" />
</ContextMenu>
</TextBox.ContextMenu>
Запустите проектОбратите внимание, что применение акселератора Shift+F10 всегда вызывает наше
контекстное меню в центре элемента редактирования.
Отметьте странное поведение
иконок: при открытии разделов меню соответствующая группа иконок исчезает
из других элементов управления. Это происходит потому, что по умолчанию логический
ресурс считается разделяемым. А это значит, что объект, объявленный
в ресурсе, при многократном использовании может появляться только в одном месте.
Чтобы это устранить, нужно объявить ресурс неразделяемым с помощью атрибута x:Shared="False" (share - разделять,
делиться;
shared - общий).
Добавьте в секцию объявления
ресурсов атрибут x:Shared="False"<Window.Resources>
<!-- File -->
<Image x:Shared="False" x:Key="iconNew" Source="Images/NewDocument.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconOpen" Source="Images/Open.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconSave" Source="Images/Save.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPageSetup" Source="Images/PrintSetup.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPrintPreview" Source="Images/PrintPreview.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPrint" Source="Images/Print.bmp" Width="16" Height="16" />
<!-- Edit -->
<Image x:Shared="False" x:Key="iconUndo" Source="Images/Edit_Undo.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconRedo" Source="Images/Edit_Redo.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconCut" Source="Images/Cut.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconCopy" Source="Images/Copy.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPaste" Source="Images/Paste.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconDelete" Source="Images/Delete.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconFind" Source="Images/Find.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconFont" Source="Images/Font.bmp" Width="16" Height="16" />
</Window.Resources>
Запустите проект -
убедитесь, что сам интерфейс функционирует нормальноИ наконец, прежде чем приступить к кодированию логики, все-таки изменим фон
иконок, просто заменив их на рисунки с прозрачным фоном. Такой набор иконок
можно найти все в том же поставляемом вместе с Visual Studio 2008 архиве VS2008ImageLibrary.zip в папке VS2008ImageLibrary\Actions\pngformat.
Для удобства этот набор скопирован в прилагаемую к работе папку Source. Это
файлы типа png и их имена имеют приставку HS (Hide or Show), например, вместо Cut.bmp имя нового файла - CutHS.png.
Добавьте в папку Images проекта из Source 14 рисунков png командой контекстного меню Add/Existing Item
Командой Find and
Replace (жест Ctrl+H) выполните замену в файле Window1.xaml всех
вхождений .bmp на HS.png
Запустите проект и
убедитесь, что фон иконок во всех элементах стал прозрачным
Прежде, чем приступить к реализации функциональности с помощью подключения
команд, потренируемся на отключении встроенных команд элемента TextBox. Обратите
внимание, что хоть мы и заменили встроенное меню TextBox на свое и соответствующие
опции в этом контекстном меню перестали работать, все-таки продолжают функционировать
встроенные жесты, типичные для текстового поля редактирования: Ctrl+Home, Ctrl+End,
Ctrl+X, Ctrl+C, Ctrl+V, Ctrl+Z, Ctrl+Y и т.д. Попробуем отключить встроенные
жесты на примере команд Cut (Ctrl+X), Copy (Ctrl+C) и Paste (Ctrl+V).
Добавьте в открывающий
дескриптор окна подписку на событие Loaded, чтобы создать обработчик для
размещения процедурного кода, который будет выполнен сразу после инициализации
элементов окна, определенных в разметке<Window x:Class="Notepad1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
>
......................................................
</Window>
Присвойте в разметке
имя элементу TextBox для возможности его кодирования в файле Window1.xaml.cs<TextBox TextWrapping="Wrap"
AcceptsReturn="True"
AcceptsTab="True"
VerticalScrollBarVisibility="Auto"
Name="txtBox1"
>
.................................................
</TextBox>
Добавьте в обработчик
события Loaded следующий код отключения встроенных жестов вырезания, копирования
и вставки в элементе текстового поляprivate void Window_Loaded(object sender, RoutedEventArgs e)
{
// Отключаем в TextBox встроенный жест Ctrl+X для команды Cut
KeyBinding keyBinding = new KeyBinding(
ApplicationCommands.NotACommand, Key.X, ModifierKeys.Control);
txtBox1.InputBindings.Add(keyBinding);
// Отключаем в TextBox встроенный жест Ctrl+C для команды Copy
keyBinding = new KeyBinding(
ApplicationCommands.NotACommand, Key.C, ModifierKeys.Control);
txtBox1.InputBindings.Add(keyBinding);
// Отключаем в TextBox встроенный жест Ctrl+V для команды Paste
keyBinding = new KeyBinding(
ApplicationCommands.NotACommand, Key.V, ModifierKeys.Control);
txtBox1.InputBindings.Add(keyBinding);
}
Ключевым элементом в данном способе отключения жестов является команда NotACommand, которая означает пустую команду. Мы отключили только источники
возбуждения встроенных команд для конкретного элемента TextBox, но сами встроенные
команды продолжают существовать с теми же самыми жестами.
Скопируйте из папки Source в корень проекта Notepad1 командой контекстного меню Add/Existing Item иконку Notepad.ico и подключите ее к окну Window1<Window x:Class="Notepad1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
>
......................................................
</Window>
Код решения поставленной задачи будет достаточно большим. Чтобы сделать его обозримым, распределим отдельные группы частичного класса по отдельным файлам в соответствии с разделами меню. Все равно компилятор их увидет и соберет в единую сборку.
В панели Solution
Explorer выделите узел проекта Notepad1 и командой Project/Add Class добавьте
три файла с именами File.cs, Edit.cs и Other.cs
Скопируйте из файла Window1.xaml.cs в каждый из этих новых файлов код подключения пространств
имен и отредактируйте заготовки частей класса Window1, которые пока будут
одинаковые, такusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
}
}
Откройте файл Window1.xaml.cs и дополните класс Window1 вспомогательным кодом, после чего он должен стать
такимusing System;
using System.Windows;
using System.Windows.Input;
using Microsoft.Win32; // Для стандартных диалогов Win32
using System.IO; // Работа с файлами и каталогами
namespace Notepad1
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Отключаем в TextBox встроенный жест Ctrl+X для команды Cut
KeyBinding keyBinding = new KeyBinding(
ApplicationCommands.NotACommand, Key.X, ModifierKeys.Control);
txtBox1.InputBindings.Add(keyBinding);
// Отключаем в TextBox встроенный жест Ctrl+C для команды Copy
keyBinding = new KeyBinding(
ApplicationCommands.NotACommand, Key.C, ModifierKeys.Control);
txtBox1.InputBindings.Add(keyBinding);
// Отключаем в TextBox встроенный жест Ctrl+V для команды Paste
keyBinding = new KeyBinding(
ApplicationCommands.NotACommand, Key.V, ModifierKeys.Control);
txtBox1.InputBindings.Add(keyBinding);
}
#region private Fields - локальные поля
//------------------------------------------------------
//
// private Fields - локальные поля
//
//------------------------------------------------------
bool IsModified = false;// Флаг изменений содержимого
string strLoadedFile; // Полное имя загруженного документа
#endregion private Fields
#region Auxiliary Methods - вспомогательные методы
//------------------------------------------------------
//
// Auxiliary Methods - вспомогательные методы
//
//------------------------------------------------------
// Метод возвращает true, если содержимое
// TextBox не требует сохранения
bool flag;
bool CheckModifiedAndSaveIt()
{
if (!IsModified)
return true;
MessageBoxResult result =
MessageBox.Show(
"Сохранить изменения?", "", // Контекст и заголовок
MessageBoxButton.YesNoCancel, // Кнопки диалога
MessageBoxImage.Question, // Иконка вопроса
MessageBoxResult.Yes // Кнопка с фокусом
);
switch (result)
{
case MessageBoxResult.Yes:
if (String.IsNullOrEmpty(strLoadedFile))
flag = DisplaySaveDialog(""); // Запись с диалогом
else
flag = SaveFile(strLoadedFile); // Просто запись
break;
case MessageBoxResult.No:
flag = true;
break;
case MessageBoxResult.Cancel:
flag = false;
break;
}
return flag;
}
// Вызывает диалоговое окно записи файла
// и возвращает true, если файл был сохранен
bool DisplaySaveDialog(string strFileName)
{
SaveFileDialog dlg = new SaveFileDialog();
dlg.Filter = "Text Documents(*.txt)|*.txt|All Files(*.*)|*.*";
dlg.FileName = strFileName;
bool result = (bool)dlg.ShowDialog(this); // Желание пользователя
if (result)
result = SaveFile(dlg.FileName); // Возможность компьютера
return result;
}
// Сохраняет документ и возвращает true при успехе
// Аргумент - полное имя файла
bool SaveFile(string strFileName)
{
try
{
File.WriteAllText(strFileName, txtBox1.Text,
System.Text.Encoding.GetEncoding(1251));
}
catch (Exception e)
{
// Ловим все исключения и выводим диалог
MessageBox.Show(
"Ошибка записи файла:\n" + e.Message, "",
MessageBoxButton.OK,
MessageBoxImage.Asterisk
);
return false;
}
strLoadedFile = strFileName;
UpdateTitle(); // Меняем заголовок окна
IsModified = false; // Нет изменений текста
return true;
}
// Диалог открытия файла возвращает true при успехе
bool DisplayOpenDialog()
{
flag = CheckModifiedAndSaveIt(); // Проверяем и сохраняем изменения
if(!flag)
return flag;
OpenFileDialog dlg = new OpenFileDialog();
dlg.Filter = "Text Documents(*.txt)|*.txt|All Files(*.*)|*.*";
bool result = (bool)dlg.ShowDialog(this); // Желание пользователя
if (result)
result = OpenFile(dlg.FileName); // Возможность компьютера
return result;
}
// Открывает файл и при успехе возвращает true
bool OpenFile(string strFileName)
{
try
{
txtBox1.Text = File.ReadAllText(strFileName,
System.Text.Encoding.GetEncoding(1251));
}
catch (Exception e)
{
// Ловим все исключения и выводим диалог
MessageBox.Show(
"Ошибка чтения файла:\n" + e.Message, "",
MessageBoxButton.OK,
MessageBoxImage.Asterisk
);
return false;
}
strLoadedFile = strFileName;
UpdateTitle(); // Меняем заголовок окна
IsModified = false; // Нет изменений текста
// Сбрасываем границы выделенного текста поля редактирования
txtBox1.SelectionStart = 0;
txtBox1.SelectionLength = 0;
return true;
}
// Коррекция заголовка окна
void UpdateTitle()
{
// Извлекаем заголовок окна из словаря ресурсов
String title = Application.Current.
Resources["ApplicationTitle1"].ToString();
//if (strLoadedFile == null || strLoadedFile.Trim() == String.Empty)
if (String.IsNullOrEmpty(strLoadedFile)) // Проще!
{
this.Title = "Untitled - " + title;
return;
}
// Извлекаем имя файла из полного пути
int startIndex = strLoadedFile.LastIndexOf('\\') + 1;
int endIndex;
// Проверяем в системном реестре настройки системы по скрытию расширения файлов
using (RegistryKey filekey = Registry.CurrentUser.CreateSubKey(
@"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"))
{
if ((filekey != null) (filekey.GetValue("HideFileExt", 0).ToString() == "0"))
{
endIndex = strLoadedFile.Length; // Нет расширения
}
else
{
endIndex = strLoadedFile.LastIndexOf('.'); // Отсекаем расширение
}
}
if (endIndex > startIndex)
{
this.Title = strLoadedFile.Substring(startIndex) +
" - " + title;
}
else
{
this.Title = strLoadedFile.Substring(startIndex, endIndex - startIndex) +
" - " + title;
}
}
#endregion Auxiliary Methods
}
}
Смысл добавленного кода подробно помечен коментариями и следует его внимательно
изучить. Ключевым полем работы кода является флаг IsModified, сигнализирующий
об изменении содержимого элемента TextBox. Поднятие флага выполним в обработчике
события TextChanged этого элемента.
В разметку TextBox добавьте регистрацию обработчика события TextChanged и создайте сам обработчик<!-- Многострочное текстовое поле редактирования -->
<TextBox TextWrapping="Wrap"
AcceptsReturn="True"
AcceptsTab="True"
VerticalScrollBarVisibility="Auto"
Name="txtBox1"
TextChanged="txtBox1_TextChanged"
>
....................................................
</TextBox>
Обработчик txtBox1_TextChanged() заполните
такprivate void txtBox1_TextChanged(object sender,
System.Windows.Controls.TextChangedEventArgs e)
{
if (IsModified)
return;
else
IsModified = true;
}
В коде вспомогательных функций используется словарь ресурсов приложения, которые мы сейчас определим.
Откройте файл разметки App.xaml и дополните его определением ресурсов для хранения неизменяемой части
заголовка окна. Областью видимости этих ресурсов будет все приложение<Application x:Class="Notepad1.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml">
<Application.Resources>
<String xmlns="clr-namespace:System;assembly=Mscorlib" x:Key="ApplicationTitle1">
Title="Window1: Управление состоянием источников команд"
</String>
<String xmlns="clr-namespace:System;assembly=Mscorlib" x:Key="ApplicationTitle2">
Title="Window2: Управление состоянием источников команд"
</String>
</Application.Resources>
</Application>
Будем мы использовать механизм команд WPF или нет, но без событий и их обработчиков
никак не обойтись. Начнем с того факта, что для решения наших задач можно создать
обработчики с одинаковой сигнатурой, определяемой делегатом пространства имен System.Windows:
public delegate void RoutedEventHandler(object sender, RoutedEventArgs e)
Имена обработчикам будем присваивать в соответствии с решаемой ими задачей.
Обработчики создадим вручную и их необязательно сразу присоединять к событиям
источников, поскольку неприсоединенные обработчики будут считаться обычными
методами класса. Наиболее полно все задачи представлены в главном меню приложения,
поэтому для каждой из них нужно создать свою группу обработчиков. Выполним
все это по порядку, размещая группы обработчиков в соответствующих частях класса Window1.
Добавьте в часть класса Window1, расположенную в файле File.cs, следующие пустые обработчики
по количеству задач меню в разделе Fileusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Обработчики источников задач File
//
//------------------------------------------------------
private void NewOnExecute(object sender, RoutedEventArgs e)
{
}
private void OpenOnExecute(object sender, RoutedEventArgs e)
{
}
private void SaveOnExecute(object sender, RoutedEventArgs e)
{
}
private void SaveAsOnExecute(object sender, RoutedEventArgs e)
{
}
private void PageSetupOnExecute(object sender, RoutedEventArgs e)
{
}
private void PrintPreviewOnExecute(object sender, RoutedEventArgs e)
{
}
private void PrintOnExecute(object sender, RoutedEventArgs e)
{
}
private void ExitOnExecute(object sender, RoutedEventArgs e)
{
}
}
}
Добавьте в часть класса Window1, расположенную в файле Edit.cs, следующие пустые обработчики
по количеству задач меню в разделе Editusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Обработчики источников задач Edit
//
//------------------------------------------------------
private void UndoOnExecute(object sender, RoutedEventArgs e)
{
}
private void RedoOnExecute(object sender, RoutedEventArgs e)
{
}
private void CutOnExecute(object sender, RoutedEventArgs e)
{
}
private void CopyOnExecute(object sender, RoutedEventArgs e)
{
}
private void PasteOnExecute(object sender, RoutedEventArgs e)
{
}
private void DeleteOnExecute(object sender, RoutedEventArgs e)
{
}
private void FindOnExecute(object sender, RoutedEventArgs e)
{
}
private void FindNextOnExecute(object sender, RoutedEventArgs e)
{
}
private void ReplaceOnExecute(object sender, RoutedEventArgs e)
{
}
private void GoToOnExecute(object sender, RoutedEventArgs e)
{
}
private void SelectAllOnExecute(object sender, RoutedEventArgs e)
{
}
}
}
Добавьте в часть класса Window1, расположенную в файле Other.cs, следующие пустые обработчики
по количеству задач меню в оставшихся разделахusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Прочие обработчики
//
//------------------------------------------------------
private void FontOnExecute(object sender, RoutedEventArgs e)
{
}
private void WordWrapOnExecute(object sender, RoutedEventArgs e)
{
}
private void AboutOnExecute(object sender, RoutedEventArgs e)
{
}
}
}
Запустите приложение
и убедитесь, что ошибок компиляции нет, но функциональности от выполненной
нами работы в нем пока не прибавилосьМы подготовили интерфейс окна Window1 и дальше наступает этап кодирования
функциональности. В окне Window1 мы откажемся от услуг механизма команд, а будем использовать
обычную технологию, основанную на обработчиках событий. Начнем постепенно подключать
обработчики к источникам задач, используя событие Click.
Подключите декларативно
в файле разметки Window1.xaml к каждому источнику события Click соответствующий
обработчик и назначьте имена элементам для последующего управления ими из
кодаОбратите внимание на некоторую некорректность работы подсказчика
кода IntelliSense, который не предлагает нам список уже созданных обработчиков.
Он ищет их в застраничном файле Window1.xaml.cs, в то время как обработчики
находятся в других файлах частичного класса. В данном случае это не слишком
большое неудобство, поскольку мнемоника составления имен у нас строго соблюдена
и мы вряд ли ошибемся. Но в больших практических проектах это следует иметь
ввиду, прежде чем разбивать класс окна по отдельным файлам.
Разметка с подключенными обработчиками и присвоенными именами будет выглядеть так (файл приводится целиком)
<Window x:Class="Notepad1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
>
<Window.Resources>
<!-- File -->
<Image x:Shared="False" x:Key="iconNew" Source="Images/NewDocumentHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconOpen" Source="Images/OpenHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconSave" Source="Images/SaveHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPageSetup" Source="Images/PrintSetupHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPrintPreview" Source="Images/PrintPreviewHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPrint" Source="Images/PrintHS.png" Width="16" Height="16" />
<!-- Edit -->
<Image x:Shared="False" x:Key="iconUndo" Source="Images/Edit_UndoHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconRedo" Source="Images/Edit_RedoHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconCut" Source="Images/CutHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconCopy" Source="Images/CopyHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPaste" Source="Images/PasteHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconDelete" Source="Images/DeleteHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconFind" Source="Images/FindHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconFont" Source="Images/FontHS.png" Width="16" Height="16" />
</Window.Resources>
<DockPanel LastChildFill="True">
<!-- Меню -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<!-- Сокращенные варианты подключения иконок с использованием статических ресурсов -->
<MenuItem Name="itemNew" Click="NewOnExecute" Header="_New" InputGestureText="Ctrl+N"
Icon="{StaticResource iconNew}" />
<MenuItem Name="itemOpen" Click="OpenOnExecute" Header="_Open..." InputGestureText="Ctrl+O"
Icon="{StaticResource iconOpen}" />
<MenuItem Name="itemSave" Click="SaveOnExecute" Header="_Save" InputGestureText="Ctrl+S"
Icon="{StaticResource iconSave}" />
<MenuItem Name="itemSaveAs" Click="SaveAsOnExecute" Header="Save _As..." />
<Separator />
<MenuItem Name="itemPageSetup" Click="PageSetupOnExecute"
Header="Page Set_up..." Icon="{StaticResource iconPageSetup}" />
<MenuItem Name="itemPrintPreview" Click="PrintPreviewOnExecute" Header="P_rint Preview"
InputGestureText="Ctrl+F2" Icon="{StaticResource iconPrintPreview}" />
<MenuItem Name="itemPrint" Click="PrintOnExecute"
Header="_Print..." InputGestureText="Ctrl+P" Icon="{StaticResource iconPrint}" />
<Separator />
<MenuItem Name="itemExit" Click="ExitOnExecute" Header="E_xit" />
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Name="itemUndo" Click="UndoOnExecute"
Header="_Undo" InputGestureText="Ctrl+Z" Icon="{StaticResource iconUndo}" />
<MenuItem Name="itemRedo" Click="RedoOnExecute"
Header="_Redo" InputGestureText="Ctrl+Y" Icon="{StaticResource iconRedo}" />
<Separator></Separator>
<MenuItem Name="itemCut" Click="CutOnExecute"
Header="Cu_t" InputGestureText="Ctrl+X" Icon="{StaticResource iconCut}" />
<MenuItem Name="itemCopy" Click="CopyOnExecute"
Header="_Copy" InputGestureText="Ctrl+C" Icon="{StaticResource iconCopy}" />
<MenuItem Name="itemPaste" Click="PasteOnExecute"
Header="_Paste" InputGestureText="Ctrl+V" Icon="{StaticResource iconPaste}" />
<MenuItem Name="itemDelete" Click="DeleteOnExecute"
Header="De_lete" InputGestureText="Del" Icon="{StaticResource iconDelete}" />
<Separator></Separator>
<MenuItem Name="itemFind" Click="FindOnExecute"
Header="_Find..." InputGestureText="Ctrl+F" Icon="{StaticResource iconFind}" />
<MenuItem Name="itemFindNext" Click="FindNextOnExecute"
Header="Find _Next" InputGestureText="F3" />
<MenuItem Name="itemReplace" Click="ReplaceOnExecute"
Header="_Replace..." InputGestureText="Ctrl+H" />
<MenuItem Name="itemGoTo" Click="GoToOnExecute"
Header="_Go To..." InputGestureText="Ctrl+G" />
<Separator></Separator>
<MenuItem Name="itemSelectAll" Click="SelectAllOnExecute"
Header="Select _All" InputGestureText="Ctrl+A" />
</MenuItem>
<MenuItem Header="F_ormat">
<MenuItem Name="itemFont" Click="FontOnExecute"
Header="_Font..." Icon="{StaticResource iconFont}" />
<Separator />
<MenuItem Name="itemWordWrap" Click="WordWrapOnExecute"
Header="_Word Wrap" IsCheckable="True" IsChecked="True" InputGestureText="Ctrl+W" />
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Name="itemAbout" Click="AboutOnExecute"
Header="_About" />
</MenuItem>
</Menu>
<!-- Панель инструментов -->
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<Button Name="btnNew" Click="NewOnExecute" Width="23" Content="{StaticResource iconNew}" />
<Button Name="btnOpen" Click="OpenOnExecute" Width="23" Content="{StaticResource iconOpen}" />
<Button Name="btnSave" Click="SaveOnExecute" Width="23" Content="{StaticResource iconSave}" />
</ToolBar>
<ToolBar>
<Button Name="btnUndo" Click="UndoOnExecute" Width="23" Content="{StaticResource iconUndo}" />
<Button Name="btnRedo" Click="RedoOnExecute" Width="23" Content="{StaticResource iconRedo}" />
<Separator />
<Button Name="btnCut" Click="CutOnExecute" Width="23" Content="{StaticResource iconCut}" />
<Button Name="btnCopy" Click="CopyOnExecute" Width="23" Content="{StaticResource iconCopy}" />
<Button Name="btnPaste" Click="PasteOnExecute" Width="23" Content="{StaticResource iconPaste}" />
<Button Name="btnDelete" Click="DeleteOnExecute" Width="23" Content="{StaticResource iconDelete}" />
</ToolBar>
<ToolBar Header="Find:">
<TextBox Width="100" />
<Button Name="btnFind" Click="FindOnExecute" Width="23" Content="{StaticResource iconFind}" />
</ToolBar>
</ToolBarTray>
<!-- Строка состояния -->
<StatusBar DockPanel.Dock="Bottom" Height="32" Name="statusBar">
<Label>Simulator Application is Loading</Label>
<Separator />
<ProgressBar Height="20" Width="100" IsIndeterminate="True" />
</StatusBar>
<!-- Многострочное текстовое поле редактирования -->
<TextBox TextWrapping="Wrap"
AcceptsReturn="True"
AcceptsTab="True"
VerticalScrollBarVisibility="Auto"
Name="txtBox1"
>
<TextBox.ContextMenu>
<ContextMenu Width="100">
<MenuItem Name="contextCut" Click="CutOnExecute"
Header="Cu_t" Icon="{StaticResource iconCut}" />
<MenuItem Name="contextCopy" Click="CopyOnExecute"
Header="_Copy" Icon="{StaticResource iconCopy}" />
<MenuItem Name="contextPaste" Click="PasteOnExecute"
Header="_Paste" Icon="{StaticResource iconPaste}" />
<MenuItem Name="contextDelete" Click="DeleteOnExecute"
Header="De_lete" Icon="{StaticResource iconDelete}" />
</ContextMenu>
</TextBox.ContextMenu>
</TextBox>
</DockPanel>
</Window>
Столько имен нам не понадобиться, но для единообразия мы, на всякий случай, промаркировали все источники задач (может пригодится).
Запустите приложение
для проверки отсутствия синтаксических ошибокСледующим шагом следует наполнить созданные заготовки обработчиков практическим кодом в соответствии с логикой работы приложения. Поскольку наша задача состоит не в создании полнофункционального приложения целиком, а в иллюстрации технологии его создания с применением механизма команд, реализуем намеченные задачи частично. Некоторые функции, ввиду их большого объема и сложности кодирования, мы только обозначим выдачей соответствующих диалоговых окон или сообщений. Управление доступностью элементов-источников задач пользовательского интерфейса пока отложим 'на потом' (или навсегда!).
Выделите корень проекта Notepad1 и командой Project/Add Reference подключите сборки System.Windows.Forms.dll и System.Drawing.dll
Модифицируйте код файла File.cs следующим образомusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
// Для сборок: System.Windows.Forms.dll, System.Drawing.dll
using sdp = System.Drawing.Printing;// Псевдоним пространства имен
using swf = System.Windows.Forms; // Псевдоним пространства имен
using PageSetupDialog = System.Windows.Forms.PageSetupDialog;// Псевдоним класса
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Обработчики источников задач File
//
//------------------------------------------------------
private void NewOnExecute(object sender, RoutedEventArgs e)
{
// Пользователь передумал или была ошибка записи изменений
if (!CheckModifiedAndSaveIt())
return;
// Изменений нет или они успешно сохранены
//txtBox1.Text = String.Empty; // Вариант I
//txtBox1.Text = ""; // Вариант II
txtBox1.Clear(); // Вариант III
strLoadedFile = null;
IsModified = false;
UpdateTitle();
txtBox1.Focus();
}
private void OpenOnExecute(object sender, RoutedEventArgs e)
{
if(DisplayOpenDialog())
txtBox1.CaretIndex = txtBox1.Text.Length;// Курсор в конец
txtBox1.Focus();// Передача фокуса
}
private void SaveOnExecute(object sender, RoutedEventArgs e)
{
if (String.IsNullOrEmpty(strLoadedFile))
DisplaySaveDialog(String.Empty);
else
SaveFile(strLoadedFile);
txtBox1.Focus();
}
private void SaveAsOnExecute(object sender, RoutedEventArgs e)
{
DisplaySaveDialog(strLoadedFile);
txtBox1.Focus();
}
private void PageSetupOnExecute(object sender, RoutedEventArgs e)
{
// Ограничемся только показом окна Windows Forms
PageSetupDialog dlg = new PageSetupDialog();
// Без настроек не работает. Зададим хотя бы по умолчанию
dlg.PageSettings = new sdp.PageSettings();
dlg.PrinterSettings = new sdp.PrinterSettings();
dlg.ShowDialog();
txtBox1.Focus();
}
private void PrintPreviewOnExecute(object sender, RoutedEventArgs e)
{
sdp.PrintDocument document = new sdp.PrintDocument();
document.DocumentName = strLoadedFile;
swf.PrintPreviewDialog dlg = new swf.PrintPreviewDialog();
dlg.Document = document;
dlg.UseAntiAlias = true;// Включить сглаживание
dlg.ShowDialog();
txtBox1.Focus();
}
private void PrintOnExecute(object sender, RoutedEventArgs e)
{
sdp.PrintDocument document = new sdp.PrintDocument();
document.DocumentName = strLoadedFile;
swf.PrintDialog dlg = new swf.PrintDialog();
dlg.Document = document;
dlg.ShowDialog();
txtBox1.Focus();
}
private void ExitOnExecute(object sender, RoutedEventArgs e)
{
if (!CheckModifiedAndSaveIt())
return; // Пользователь передумал выходить
Close();
}
}
}
Обратите внимание на использование псевдонимов пространств имен добавленных
к проекту библиотечных сборок. Еще раз вспомним, что при совместном применении
пользовательских интерфейсов WPF и Windows Forms имена типов одной из
технологий следует прописывать полностью или использовать псевдонимы. Иначе
в коде могут возникнуть конфликты имен, а если компилятор их не обнаружит,
то и серьезные ошибки времени выполнения.
Запустите приложение,
испытайте правильность функционирования обработчиков, изучите их кодПока в приложение закрался один существенный недостаток. Когда документ содержит
несохраненные изменения и пользователь завершает работу приложения по нашей
команде Exit, то все в порядке - приложение извещает о необходимости их сохранить.
Но когда окно закрывается по системной кнопке, то извещение отсутствует. Исправим
это, для чего воспользуемся событием Closing. В отличии от Closing событие Closed возбудается, когда отменить закрытие окна уже невозможно, а можно
только что-то
доделать.
Присоедините к открывающему
дескриптору окна Window1 в файле Window1.xaml обработчик события Closing,
которое будет возбуждаться при попытке закрытия окна любым способом<Window x:Class="Notepad1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
Closing="Window_Closing"
>
Заполните обработчик
события Closing следующим кодомprivate void Window_Closing(object sender,
System.ComponentModel.CancelEventArgs e)
{
if (!CheckModifiedAndSaveIt())
{
e.Cancel = true;
return; // Пользователь передумал выходить
}
}
Запустите приложение
- теперь сообщение о несохраненных изменениях выводится и при попытке закрытия
окна системной кнопкойОбратите внимание, что при закрытии окна через наше меню задачей Exit на
кнопку отказа от сохранения надо щелкать 2 раза. Это связано с тем, что в обработчике
события Closing проверка повторяется. Значит при выходе через меню
проверку в обработчике события Closing нужно блокировать с помощью флага.
Добавьте в класс Window1 поле-флаг _IsExitItem и модифицируйте соответствующим образом обработчикиbool _IsExitItem = false;
private void ExitOnExecute(object sender, RoutedEventArgs e)
{
if (!CheckModifiedAndSaveIt())
return; // Пользователь передумал выходить
_IsExitItem = true;
Close();
}
private void Window_Closing(object sender,
System.ComponentModel.CancelEventArgs e)
{
/*
// Эта проверка была бы надежнее
if(_IsExitItem)
return;
*/
// !_IsExitItem должен в условии стоять первым
if (!_IsExitItem !CheckModifiedAndSaveIt())
{
e.Cancel = true;
_IsExitItem = false;
return; // Пользователь передумал выходить
}
}
В последнем обработчике есть один поучительный нюанс: если в условии проверку
значения флага поставить последним, то код будет реагировать на наши нововведения.
Это происходит потому, что логическое умножение проверяется компилятором слева
направо до первого ложного значения. В нашем случае если флаг _IsExitItem==true,
то функция CheckModifiedAndSaveIt() выполняться уже не будет. Такой код менее
надежен, поскольку зависит от компилятора, да и мы (или сопровождающий программист)
можем случайно переставить члены выражения местами. Поэтому лучше заменить
этот код на более ясный, как показано в коментариях.
Запустите приложение
и испытайте работу закрытия окна при несохраняемых измененияхЗадачи раздела меню Edit, связанные с использованием буфера обмена, реализовать
достаточно легко, поскольку вся необходимая функциональность уже заложена в
элементе TextBox. Одними из первых подключим откаты Undo и Redo. Можно было
бы создать свой механизм хранения откатов, но мы воспользуемся встроенным в TextBox журналом откатов. Он поддерживается методами Undo() и Redo(). Все члены
класса TextBox можно посмотреть в MSDN на русском языке по ссылке
http://msdn.microsoft.com/ru-ru/library/system.windows.controls.textbox_members.aspx
Еще раз вспомним, что мы пока не управляем доступностью элементов-источников задач.
В файле Edit.cs заполните
часть обработчиков следующим кодомpartial class Window1
{
//------------------------------------------------------
//
// Обработчики источников задач Edit
//
//------------------------------------------------------
private void UndoOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Undo();
}
private void RedoOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Redo();
}
private void CutOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Cut();
// Вариант
//Clipboard.SetText(txtBox1.SelectedText);
//txtBox1.SelectedText = "";
}
private void CopyOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Copy();
// Вариант
//Clipboard.SetText(txtBox1.SelectedText);
}
private void PasteOnExecute(object sender, RoutedEventArgs e)
{
// Если в буфере содержатся данные текстового формата
if (Clipboard.ContainsText())
txtBox1.Paste();
}
private void DeleteOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.SelectedText = String.Empty;
}
private void FindOnExecute(object sender, RoutedEventArgs e)
{
}
private void FindNextOnExecute(object sender, RoutedEventArgs e)
{
}
private void ReplaceOnExecute(object sender, RoutedEventArgs e)
{
}
private void GoToOnExecute(object sender, RoutedEventArgs e)
{
}
private void SelectAllOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.SelectAll();
}
}
Оставшиеся обработчики задач меню Edit требуют более сложного кода с использованием
немодальных диалоговых окон, которые должны располагаться всегда поверх основного
окна с редактируемым текстом. Попробуем последовательно реализовать их.
Создадим диалоговое окно и назначим ему в качестве владельца основное окно Window1. Окно, имеющее владельца, всегда располагается поверх него, свертывается
и закрывается вместе с ним. Именно так и должно вести себя проектируемое окно Find and Replace в режиме runtime (при выполнении).
В панели Solution
Explorer выделите узел проекта Notepad1 и командой меню Project/Add Window добавьте новое окно с именем FindAndReplaceDialog
Заполните файл FindAndReplaceDialog.xaml разметкой создания интерфейса диалогового окна<Window x:Class="Notepad1.FindAndReplaceDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Find and Replace"
WindowStartupLocation="CenterOwner"
SizeToContent="WidthAndHeight"
ResizeMode="NoResize"
ShowInTaskbar="false"
Activated="OnActivated"
Background="{StaticResource DialogBackgroundBrush}"
>
<Grid ShowGridLines="False" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Контейнер для кнопок -->
<StackPanel
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="2"
Margin="5"
>
<!-- Задаем отступы кнопок -->
<StackPanel.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="Margin" Value="3" />
</Style>
</StackPanel.Resources>
<Button
MinWidth="75"
MinHeight="23"
Name="_findNext"
IsDefault="True"
IsEnabled="False"
Click="FindNextClicked"
Content="_Find Next" />
<Button
MinWidth="75"
MinHeight="23"
Name="_replace"
Visibility="Collapsed"
Click="ReplaceClicked"
Content="_Replace" />
<Button
MinWidth="75"
MinHeight="23"
Name="_replaceAll"
Click="ReplaceAllClicked"
Visibility="Collapsed"
Content="Replace _All" />
<Button
MinWidth="75"
MinHeight="23"
Click="CancelClicked"
IsCancel="True"
Content="Cancel" />
</StackPanel>
<!-- Текстовые метки и текстовые поля в левом верхнем квадранте -->
<Grid
ShowGridLines="False"
Grid.Row="0"
Grid.Column="0"
Margin="5 10 0 0" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Label
Grid.Row="0"
Grid.Column="0"
VerticalAlignment="Center"
Target="{Binding ElementName=_findWhat}"
Content="Fi_nd what:" />
<TextBox
Name="_findWhat"
Grid.Row="0"
Grid.Column="1"
Width="150"
MaxLength="100"
Margin="0 5 0 5"
AcceptsReturn="False"
TextChanged="FindTextChanged" />
<Label
Name="_replaceLabel"
Grid.Row="1"
Grid.Column="0"
VerticalAlignment="Center"
Target="{Binding ElementName=_replaceWith}"
Visibility="Collapsed"
Content="Re_place with:" />
<TextBox
Name="_replaceWith"
Grid.Row="1"
Grid.Column="1"
Width="150"
MaxLength="100"
VerticalAlignment="Center"
Visibility="Collapsed"
AcceptsReturn="False" />
</Grid>
<!-- Секция установки регистра и направления поиска для замены -->
<Grid
Grid.Row="1"
Grid.Column="0"
>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!-- Установка регистра -->
<CheckBox
Name="_matchCase"
Grid.Column="0"
Margin="8 0 10 10"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
IsChecked="False"
Focusable="True"
Content="Match _case" />
<!-- Группа радиокнопок направления поиска -->
<GroupBox
Name="_directionGroupBox"
Header="Direction"
Grid.Column="1"
HorizontalAlignment="Right"
Margin="0 10 0 10">
<StackPanel Orientation="Horizontal" Margin="10 20 10 10">
<RadioButton
Name="_findDown"
IsChecked="True"
Content="_Down" />
<RadioButton
Name="_findUp"
Margin="8 0 0 0"
Content="_Up" />
</StackPanel>
</GroupBox>
</Grid>
</Grid>
</Window>
В настройках окна мы устанавливаем цвет фона из статического ресурса параметром
Background="{StaticResource DialogBackgroundBrush}"
Обратите внимание и
на другие настройки интерфейса окна и элементов
Добавьте в ресурсы
приложения файла App.xaml следующий код<Application x:Class="Notepad1.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="FindAndReplaceDialog.xaml">
<Application.Resources>
<String xmlns="clr-namespace:System;assembly=Mscorlib" x:Key="ApplicationTitle1">
"Window1: Управление состоянием источников команд"
</String>
<String xmlns="clr-namespace:System;assembly=Mscorlib" x:Key="ApplicationTitle2">
"Window2: Управление состоянием источников команд"
</String>
<SolidColorBrush x:Key="DialogBackgroundBrush"
Color="{x:Static SystemColors.ControlColor}" />
</Application.Resources>
</Application>
Обратите внимание, что временно для просмотра интерфейса в режиме выполнения
мы назначили диалоговое окно стартовым окном приложения. Чуть позже мы вернем
класс Window1 в категорию стартовых, а пока нам нужно просто посмотреть, как
выглядит созданный интерфейс.
Запустите приложение Notepad1 - начальный интерфейс для задачи Find должен быть таким
В настройках элементов разметки, связанных с задачей Replace, мы использовали
значение атрибута Visibility=" Collapsed ". Есть еще значения Visibility=" Hidden " и
Visibility=" Visible " (по умолчанию). Hidden - элемент скрыт
(не отображается, но занимает место), Collapsed - элемент свернут (не отображается
и не занимает место).
Заполните кодовую часть
в файле FindAndReplaceDialog.xaml.cs следующим образом (...следующим 'светлым
образом' - шутка!)using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Notepad1
{
public partial class FindAndReplaceDialog : Window
{
public FindAndReplaceDialog()
{
InitializeComponent();
// Начальная доступность кнопок, хотя кнопки Replece
_findNext.IsEnabled = _replace.IsEnabled = _replaceAll.IsEnabled
= !String.IsNullOrEmpty(_findWhat.Text);
}
#region Открытые свойства - обертки закрытых полей
//------------------------------------------------------
//
// Открытые свойства - обертки закрытых полей
//
//------------------------------------------------------
// Содержимое текстового поля _findWhat
public string FindWhat
{
get
{
return _findWhat.Text;
}
set
{
_findWhat.Text = value;
}
}
// Содержимое текстового поля _replaceWith
public string ReplaceWith
{
get
{
return _replaceWith.Text;
}
set
{
_replaceWith.Text = value;
}
}
// Определение состояния флага учета регистра
public bool? MatchCase { get { return _matchCase.IsChecked; } }
// Определение состояния радиокнопки направления поиска назад
public bool? SearchUp { get { return _findUp.IsChecked; } }
// Управление видимостью интерфейса замены
// Hidden - элемент скрыт (не отображается, но занимает место)
// Collapsed - элемент свернут (не отображается и не занимает место)
public bool ShowReplace
{
get { return _replaceWith.Visibility == Visibility.Visible; }
set
{
Visibility show;
if (value)
{
// Отобразить
show = Visibility.Visible;
_directionGroupBox.Visibility = Visibility.Collapsed;
_findDown.IsChecked = true;
}
else
{
// Свернуть
show = Visibility.Collapsed;
_directionGroupBox.Visibility = Visibility.Visible;
}
_replaceLabel.Visibility = _replaceWith.Visibility =
_replace.Visibility = _replaceAll.Visibility = show;
}
}
#endregion Общедоступные свойства - обертки полей
#region Открытые события для обработки в основном классе
//------------------------------------------------------
//
// Открытые события для обработки в основном классе Window1
// Обеспечивают взаимодействие диалогового окна с владельцем
//
//------------------------------------------------------
// Объявляем немаршрутизованные события
public event EventHandler FindNext;
public event EventHandler Replace;
public event EventHandler ReplaceAll;
#endregion Открытые события для обработки в основном классе Window1
#region Закрытые обработчики
//------------------------------------------------------
//
// Закрытые методы
// При возбуждении событий первый параметр - ссылка на диалог,
// которую в основном классе приведем к самому диалоговому окну
//
//------------------------------------------------------
void OnActivated(object sender, EventArgs e)
{
_findWhat.Focus();
}
private void FindNextClicked(object sender, RoutedEventArgs e)
{
// Если на событие подписались, возбуждаем его
if (FindNext != null)
{
FindNext(this, EventArgs.Empty);
}
}
private void ReplaceClicked(object sender, RoutedEventArgs e)
{
// Возбуждаем событие, если для него существует обработчик
if (Replace != null)
{
Replace(this, EventArgs.Empty);
}
}
private void ReplaceAllClicked(object sender, RoutedEventArgs e)
{
// Проверяем наличие обработчика и возбуждаем событие
if (ReplaceAll != null)
{
ReplaceAll(this, EventArgs.Empty);
}
}
private void CancelClicked(object sender, RoutedEventArgs e)
{
this.Close();
}
private void FindTextChanged(object sender, TextChangedEventArgs e)
{
// Управление доступность кнопок в зависимости от текстового поля
_findNext.IsEnabled = _replace.IsEnabled = _replaceAll.IsEnabled
= !String.IsNullOrEmpty(_findWhat.Text);
}
#endregion Закрытые обработчики
}
}
При щелчке на кнопках собственные
обработчики кнопок возбуждают в диалоге события, которые будет прослушивать
основное окно, перехватывать их и обрабатывать своими обработчиками. При объявлении
свойств MatchCase и SearchUp использован тип " bool?", поскольку элементы CheckBox и RadioButton имеют три состояния: IsChecked=" False ", IsChecked=" True " и
IsChecked=" {x:Null} ".
Для выборки только булевых состояний при программном управлении в клиентском
коде этими элементами через открытые свойства нужно будет применять явное приведение
типов. Например, caseFlag=(bool)_dlg.MatchCase;, где _dlg - это экземпляр класса FindAndReplaceDialog в клиенте.
Запустите приложение
для проверки отсутствия синтаксических ошибокТеперь нужно подключить диалоговое окно Find and Replace к основному окну Window1 для совместной работы и наделить его соответствующей функциональностью.
Но прежде надо вновь назначить Window1 стартовым окном приложения.
Откройте файл App.xaml приложения Notepad1 и внесите изменения в открывающий дескриптор:Было StartupUri="FindAndReplaceDialog.xaml" Стало StartupUri="Window1.xaml"
Поскольку код программного управления диалоговым окном Find and Replace будет
несколько великоват, разместим его в отдельном файле с именем EditFind.cs.
В панели Solution
Explorer выделите узел проекта Notepad1 и вызовите командой меню Project/Add New Item одноименное
диалоговое окно оболочки, которое настройте так
>
После щелчка на кнопке Add мастер добавит пустой файл в текущий проект решения.
Заполните файл EditFind.cs следующим
кодомusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
internal FindAndReplaceDialog _dlg = null; // Для видимости в обработчиках
string _findString, _replaceString; // Для видимости в обработчиках
void CreateDialog()
{
if (_dlg != null)// Уже существует
{
// Если есть выделенное, обновляем прежнее
if (!String.IsNullOrEmpty(txtBox1.SelectedText))
_findString = _dlg._findWhat.SelectedText = txtBox1.SelectedText;
_dlg._findWhat.Focus();
return;
}
// Создать заново
_dlg = new FindAndReplaceDialog();
_dlg.Owner = this; // Привязываем диалог к владельцу
_dlg.Show();// Немодальное, поэтому не перехватывает управление
// Продолжаем настраивать
_dlg._findWhat.Focus();
// Если есть выделенное, обновляем прежнее
if (!String.IsNullOrEmpty(txtBox1.SelectedText))
_findString = _dlg._findWhat.SelectedText = txtBox1.SelectedText;
_dlg.ReplaceWith = _replaceString;
// Анонимные обработчики
_dlg.FindNext += delegate(object sender, EventArgs args)
{
FindNextExec();
}; //!!!
_dlg.Replace += delegate(object sender, EventArgs args)
{
ReplaceExec();
}; //!!!
_dlg.ReplaceAll += delegate(object sender, EventArgs args)
{ // У Петцольда (WPF с.466) есть иной вариант этого обработчика
_replaceString = _dlg.ReplaceWith;
txtBox1.SelectionStart = 0;
txtBox1.SelectionLength = 0;
while (FindNextExec())
{
using (txtBox1.DeclareChangeBlock())
{
txtBox1.SelectedText = _replaceString;
txtBox1.SelectionLength = _replaceString.Length;
}
}
txtBox1.SelectionStart = 0;
txtBox1.SelectionLength = 0;
}; //!!!
_dlg.Closed += delegate(object sender, EventArgs args)
{
_dlg = null;
}; //!!! Точка с запятой обязательна - заканчивает строку
}
bool FindNextExec()
{
int indexStart, indexFind;// Откуда начать и начало следующего
_findString = _dlg.FindWhat; // Извлекаем текст поиска
// Учет регистра при поиске, однострочный условный оператор
StringComparison strComp = (bool)_dlg.MatchCase ? StringComparison.Ordinal :
StringComparison.OrdinalIgnoreCase;
if ((bool)_dlg.SearchUp)// Ищем вверх
{
indexStart = txtBox1.SelectionStart - 1;
indexFind = txtBox1.Text.LastIndexOf(_findString, indexStart, strComp);
}
else // Ищем вниз
{
indexStart = txtBox1.SelectionStart + txtBox1.SelectionLength;
indexFind = txtBox1.Text.IndexOf(_findString, indexStart, strComp);
}
// Анализируем и принимаем решение
if (indexFind != -1)
{
txtBox1.Select(indexFind, _findString.Length);// Выделяем найденное
txtBox1.Focus();
return true;
}
else
{
MessageBox.Show("Текст \"" + _findString + "\" не найден!",
this.Title, MessageBoxButton.OK, MessageBoxImage.Information);
txtBox1.Focus();
return false;
}
}
private void ReplaceExec()
{
// Извлекаем тексты поиска и замены
_findString = _dlg.FindWhat;
_replaceString = _dlg.ReplaceWith;
// Учет регистра при поиске, однострочный условный оператор
StringComparison strComp = (bool)_dlg.MatchCase ? StringComparison.Ordinal :
StringComparison.OrdinalIgnoreCase;
if (_findString.Equals(txtBox1.SelectedText, strComp))
txtBox1.SelectedText = _replaceString;
FindNextExec();
}
}
}
Привязать к владельцу диалоговое окно нужно раньше, чем оно будет нарисовано, иначе заданная опция центрирования относительно владельца действовать не будет!
Код получился (на мой
взгляд!) интересным и поучительным - попробуйте разобраться
в немДля разнообразия, чтобы не придумывать новые имена обработчикам, которые
все равно как методы нигде далее сами вызывать не будем (кроме как автоматически
через события), мы применили синтаксис анонимных обработчиков.
Тот код, который нам придется вызывать не только через события диалогового окна,
но и через интерфейсные элементы основного окна Window1, упакован в отдельные
функции.
Обратите внимание, что при объявлении ссылки на экземпляр окна мы применили
модификатор доступности internal (внутренний).
Этот оператор обычно используется для типов (а не членов
типа), видимость которых нужно ограничить только текущей сборкой. В
нашем случае это ни на что не влияет и его можно убрать или заменить на private,
но интересно отметить вот что...
Откройте панель Class View в разделе View оболочки и посмотрите на пиктограммы членов класса Window1,
который мы как раз сейчас и расширяем
Оказывается, что те члены-поля класса, которые мы применили как элементы в
разметке, считаются с видимостью internal (конверт на пиктограмме). А все члены
класса, которые мы объявили в процедурном коде без указания
Теперь нам осталась самая малость - подключить готовый код функциональности Find and Replace к обработчикам
интерфейсных элементов в основном окне и испытать его работу. Заготовки обработчиков
мы создавали ранее и они находятся в файле Edit.cs.
Добавьте в соответствующие
обработчики файла Edit.cs следующий кодprivate void FindOnExecute(object sender, RoutedEventArgs e)
{
CreateDialog();
}
private void FindNextOnExecute(object sender, RoutedEventArgs e)
{
if (_dlg == null)
CreateDialog();
else
FindNextExec();
}
private void ReplaceOnExecute(object sender, RoutedEventArgs e)
{
if (_dlg == null)
{
CreateDialog();
_dlg.ShowReplace = true;
}
else if (_dlg.ShowReplace == false)
_dlg.ShowReplace = true;
else
ReplaceExec();
}
private void GoToOnExecute(object sender, RoutedEventArgs e)
{
}
Запустите проект Notepad1 и испытайте функциональность Find
and Replace. Разберитесь с кодомНачальный интерфейс диалогового окна для задачи Replace будет таким

Когда диалоговое окно Find and Replace создано, оно размещается поверх своего
родителя. Кнопки диалогового окна возбуждают события, которые обрабатываются
кодом основного окна, содержащего текст. Тот же самый код может вызываться
и в обработчиках основного окна, если пользователь управляет через интерфейсные
элементы основного окна. Этот подход чем-то напоминает автомобиль с дублирующими
педалями.
На панели инструментов осталось незадействованным текстовое поле поиска, обозначенное в разметке как
<ToolBar Header="Find:">
<TextBox Width="100" />
<Button Name="btnFind" Click="FindOnExecute" Width="23" Content="{StaticResource iconFind}" />
</ToolBar>
Я оставил его в покое (надоело), попробуйте сами, если хотите, использовать
его в коде, но для этого прежде всего ему нужно присвоить имя, например, txtFind.
Ну а лучше - пусть болтается для солидности.
Следующим шагом реализуем последнюю задачу раздела Edit в меню нашего приложения
- Go To... Как видно из запланированного (опрометчиво) нами многоточия
после названия задачи, это будет опять связано с разработкой диалогового окна.
Причем, это окно должно быть модальным, потому что выполняет одноразовую задачу.
В панели Solution
Explorer выделите узел проекта Notepad1 и добавьте командой Project/Add Window
новое окно WPF с именем GoToDialog.xaml
>
Заполните файл GoToDialog.xaml следующей
разметкой<Window x:Class="Notepad1.GoToDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
SizeToContent="WidthAndHeight"
WindowStartupLocation="CenterOwner"
ShowInTaskbar="false"
Title="Go To Line"
ResizeMode="NoResize"
Activated="OnActivated"
>
<StackPanel
Background="{StaticResource DialogBackgroundBrush}"
>
<StackPanel
Orientation="Vertical"
Margin="5"
>
<Label
Name="_rangeNumber"
Margin="0 0 8 3"
VerticalAlignment="Bottom"
Content="_Line number" />
<TextBox
Name="_lineNumberTextBox"
TabIndex="0"
MinWidth="220"
Margin="5,0" />
</StackPanel>
<StackPanel
Orientation="Horizontal"
Margin="5,0,5,5" HorizontalAlignment="Right">
<Button
Click="OkClicked"
IsDefault="true"
MinWidth="75"
MinHeight="23"
Content="OK" />
<Button
Click="CancelClicked"
IsCancel="true"
Margin="15 0 5 0"
MinWidth="75"
MinHeight="23"
Content="Cancel" />
</StackPanel>
</StackPanel>
</Window>
Представление окна в графическом конструкторе по данной разметке будет таким

Обратите внимание, что для кнопки OK свойство IsDefault="true",
чтобы пользователь мог закрыть окно клавишей Enter, а для второй кнопки IsCancel=true - чтобы выйти по клавише Esc.
Пройдитесь по разметке
и командой Navigate to Event Handler контекстного меню для
записей событий создайте заготовки обработчиков в файле процедурного кода GoToDialog.xaml.csДля тренировки воспользуемся способом хранения и извлечения некоторых сообщений в ресурсах сборки.
В панели Solution
Explorer вызовите контекстное меню для узла проекта и добавьте командой Add/New Item текстовый файл с любым именем (оставьте по умолчанию)
>
В панели Solution
Explorer вызовите контекстное меню на созданном текстовом файле и командой Rename присвойте ему новое имя StringTable.ru-ru.resTextПервая часть имени может быть произвольной, а два остальных расширения нужно строго соблюсти. Одно означает культуру, а второе служебное для менеджера ресурсов.
В панели Solution
Explorer выделите файл StringTable.ru-ru.resText и в панели Properties установите
для него следующие директивы:
Заполните файл StringTable.ru-ru.resText следующими парами ключ=значениеGotoErrorDialogTitle=Go To Line Error GotoErrorMsgFormat=Текстовое поле должно\nсодержать целое число GotoErrorMsgEmpty=Введите число в текстовое поле GotoErrorMsgRange=Выход за границы диапазона
Заполните файл GoToDialog.xaml.cs следующим процедурным кодомusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
// Дополнительные подключения пространств имен
using System.Resources;
using System.Reflection;
namespace Notepad1
{
public partial class GoToDialog : Window
{
public GoToDialog()
{
InitializeComponent();
// Сохраняем первое значение Label
_rangeOrigin = _rangeNumber.Content.ToString();
}
// Объявляем событие для прослушивания в основном окне
public event EventHandler GotoActivate;
// Закрытые поля класса
int _lineNumber = 0;
int _maxLineNumber;
ResourceManager res =
new ResourceManager("Notepad1.StringTable",
Assembly.GetExecutingAssembly());
String _rangeOrigin;
// Свойства доступа
public int LineNumber
{
get { return _lineNumber; }
set
{
_lineNumber = value;
_lineNumberTextBox.Text = _lineNumber.ToString();
}
}
public int MaxLineNumber
{
get { return _maxLineNumber; }
set { _maxLineNumber = value; }
}
private void OnActivated(object sender, EventArgs e)
{
// Если существуют обработчики, инициируем событие
if (GotoActivate != null)
GotoActivate(this, EventArgs.Empty);
_rangeNumber.Content = _rangeOrigin +
String.Format(" (1 - {0}):", _maxLineNumber);
_lineNumberTextBox.Focus();
_lineNumberTextBox.SelectAll();// Выделяем содержимое
}
private void OkClicked(object sender, RoutedEventArgs e)
{
// Не закрывать диалог, пока есть ошибки
if (string.IsNullOrEmpty(_lineNumberTextBox.Text))
{
MessageBox.Show(res.GetString("GotoErrorMsgEmpty"),
res.GetString("GotoErrorDialogTitle"));
return;
}
if (!int.TryParse(_lineNumberTextBox.Text, out _lineNumber))
{
MessageBox.Show(res.GetString("GotoErrorMsgFormat"),
res.GetString("GotoErrorDialogTitle"));
return;
}
if (LineNumber > _maxLineNumber || LineNumber <= 0)
{
MessageBox.Show(res.GetString("GotoErrorMsgRange"),
res.GetString("GotoErrorDialogTitle"));
return;
}
this.DialogResult = true;
this.Close();
}
private void CancelClicked(object sender, RoutedEventArgs e)
{
this.DialogResult = false;
this.Close();
}
}
}
Обратите внимание на то, как мы в коде извлекаем ресурсы, вложенные (Embedded)
ранее в текстовый файл StringTable.ru-ru.resText.
Подключите диалоговое
окно Go To к обработчику главного окна в файле Edit.cs (для удобства
код файла приводится полностью)using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Обработчики источников задач Edit
//
//------------------------------------------------------
private void UndoOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Undo();
}
private void RedoOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Redo();
}
private void CutOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Cut();
// Вариант
//Clipboard.SetText(txtBox1.SelectedText);
//txtBox1.SelectedText = "";
}
private void CopyOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Copy();
// Вариант
//Clipboard.SetText(txtBox1.SelectedText);
}
private void PasteOnExecute(object sender, RoutedEventArgs e)
{
// Если в буфере содержатся данные текстового формата
if (Clipboard.ContainsText())
txtBox1.Paste();
}
private void DeleteOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.SelectedText = String.Empty;
}
private void FindOnExecute(object sender, RoutedEventArgs e)
{
CreateDialog();
}
private void FindNextOnExecute(object sender, RoutedEventArgs e)
{
if (_dlg == null)
CreateDialog();
else
FindNextExec();
}
private void ReplaceOnExecute(object sender, RoutedEventArgs e)
{
if (_dlg == null)
{
CreateDialog();
_dlg.ShowReplace = true;
}
else if (_dlg.ShowReplace == false)
_dlg.ShowReplace = true;
else
ReplaceExec();
}
#region Задача Go To
private void GoToOnExecute(object sender, RoutedEventArgs e)
{
GoToDialog dlg = new GoToDialog();
dlg.Owner = this;
dlg.LineNumber = CaretLineNumber;
/***** Врет для завернутых строк Word Wrap! ******************
dlg.MaxLineNumber = txtBox1.LineCount;
/*************************************************************/
dlg.MaxLineNumber = GetMaxNumber();
// Параметры анонимного обработчика "от фонаря",
// все равно не используем
dlg.GotoActivate += delegate(object sender2, EventArgs e2)
{
// Заполняем текстовое поле диалога текущей строкой
dlg.LineNumber = CaretLineNumber;
};
// Запускаем в модальном режиме. Ждет и закрывается
if (dlg.ShowDialog() == true)
{
// Устанавливаем каретку (курсор) в начало строки
CaretLineNumber = dlg.LineNumber;
}
txtBox1.Focus();
}
int GetMaxNumber()
{
int count = 0;
int pos = 0;
int caretPos = txtBox1.Text.Length + 1;
while (pos < caretPos)
{
count++; // Счетчик строк
pos = txtBox1.Text.IndexOf("\r\n", pos);
if (pos != -1) // Нашли очередную пару
pos += 2; // Сдвигаемся правее найденных
else
break;// Больше нет
}
return count;
}
int CaretLineNumber
{
get
{
/***** Врет для завернутых строк Word Wrap! ******************
int caretPos = txtBox1.SelectionStart;
return (txtBox1.GetLineIndexFromCharacterIndex(caretPos) + 1);
/*************************************************************/
int count = 0;
int pos = 0;
int caretPos = txtBox1.SelectionStart + 1; //txtBox1.CaretIndex
while (pos < caretPos)
{
count++; // Счетчик строк
pos = txtBox1.Text.IndexOf("\r\n", pos);// \n - перевод строки
if (pos != -1) // Нашли очередную пару
pos += 2; // Сдвигаемся правее найденных
else
break;// Больше нет
}
return count;
}
set
{
value = value - 1;
int count = 0;
int pos = 0;
while (count < value) // Пока не превышает заданную
{
pos = txtBox1.Text.IndexOf("\r\n", pos);// \r - возврат каретки
if (pos != -1) // Нашли очередную пару
{
count++; // Счетчик строк
pos += 2; // Сдвигаемся правее найденных
}
else
{
pos = txtBox1.Text.Length;
break;
}
}
// Позиционируем курсор
txtBox1.SelectionStart = pos;
txtBox1.SelectionLength = 0;
}
}
#endregion Задача Go To
private void SelectAllOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.SelectAll();
}
}
}
Запустите проект и
испытайте введенную функциональность Go To. Разберитесь с кодомВ нашем приложении остались еще три запланированные задачи, обработчики которых
мы разместили в файле Other.cs. Задачи Font и About требуют создания диалоговых
окон. Чтобы не усложнять себе жизнь и не отвлекаться, ограничимся только
простым показом стандартных диалогов.
Заполните обработчики
в файле Other.cs следующим кодомusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Прочие обработчики
//
//------------------------------------------------------
private void FontOnExecute(object sender, RoutedEventArgs e)
{
System.Windows.Forms.FontDialog dlg =
new System.Windows.Forms.FontDialog();
switch (dlg.ShowDialog())
{
case System.Windows.Forms.DialogResult.Abort:
case System.Windows.Forms.DialogResult.Cancel:
case System.Windows.Forms.DialogResult.Ignore:
case System.Windows.Forms.DialogResult.No:
case System.Windows.Forms.DialogResult.None:
case System.Windows.Forms.DialogResult.OK:
case System.Windows.Forms.DialogResult.Retry:
case System.Windows.Forms.DialogResult.Yes:
break;
}
}
private void WordWrapOnExecute(object sender, RoutedEventArgs e)
{
if (itemWordWrap.IsChecked)
txtBox1.TextWrapping = TextWrapping.Wrap;
else
txtBox1.TextWrapping = TextWrapping.NoWrap;
}
private void AboutOnExecute(object sender, RoutedEventArgs e)
{
MessageBox.Show("Сведения об авторах...\n" +
"Сведения о программе...", "About");
}
}
}
Запустите проект и
проверьте добавленную функциональностьОбратите внимание, что когда заворот строк выключен, то правая часть широкой строки уходит за рамки текстового элемента и становится невидимой. Текстовый элемент поля редактирования по умолчанию настроен так, что вертикальная скролирующая линейка появляется автоматически, когда текст не вмещается по вертикали. Но горизонтальная полоса скролирования по умолчанию отключена. Исправим это.
Добавьте в разметку
открывающего дескриптора элемента txtBox1 в файле Window1.xaml следующий
атрибут<!-- Многострочное текстовое поле редактирования -->
<TextBox TextWrapping="Wrap"
AcceptsReturn="True"
AcceptsTab="True"
VerticalScrollBarVisibility="Auto"
Name="txtBox1"
TextChanged="txtBox1_TextChanged"
HorizontalScrollBarVisibility="Auto"
>
Запустите проект и
проверьте добавленную функциональностьЕсли внимательно посмотреть на работающее приложение, то можно заметить один
неприятный нюанс. Когда по щелчку мыши вызываются диалоговые окна и они перекрывают
элементы меню или кнопки, то после закрытия диалогов интерфейсные элементы
не перерисовываются. Это происходит потому, что элементы теряют фокус за
счет переключения на другое окно и мышь уходит (leave) с них не по установленной
схеме. Как заставить окно WPF перерисоваться (paint), я так и не нашел (пока),
поэтому ничего не оставалось, как применить следующий 'убогий' способ...
В файле Window1.xaml добавьте в открывающий дескриптор окна подписку на событие Activated<Window x:Class="Notepad1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
Closing="Window_Closing"
Activated="Window_Activated"
>
.........................................
</Window>
Создайте обработчик Window_Activated() в файле Window1.xaml
.cs и заполните его такprivate void Window_Activated(object sender, EventArgs e)
{
this.Height = this.ActualHeight - 1;
this.Height = this.ActualHeight + 1;
this.txtBox1.Focus();
}
Испытайте приложение
- окно снизу немного дергается, но возможно пользователь этого не заметит
(кто сейчас смотрит под ноги!), зато интерфейсные элементы "не залипают"Когда мы создавали источники - элементы меню, то бодро прописали вместе с названиями задач и жесты клавиатурного ввода. Но ни один из них в настоящий момент не работает, поскольку не подкреплен соответствующим механизмом. Далее мы этот же пример повторим с применением команд, а там, мы говорили, эта функциональность уже встроена. Но на данном этапе нам такую возможность придется реализовывать вручную, и мы это сейчас выполним - для тренировки!
Добавьте к узлу текущего
проекта пустой файл KeyGestures.cs
>
Заполните файл KeyGestures.cs следующим кодомusing System;
using System.Windows;
using System.Windows.Input;
using System.Collections.Generic;// Для Dictionary<TKey, TValue>
namespace Notepad1
{
partial class Window1
{ // Еще один вариант в Петцольд, WPF, с.316 !!!
// Определяем ассоциативный словарь
Dictionary<KeyGesture, RoutedEventHandler> gests =
new Dictionary<KeyGesture, RoutedEventHandler>();
void CreateGestures()
{
// File
gests.Add(new KeyGesture(Key.N, ModifierKeys.Control), NewOnExecute);//_New
gests.Add(new KeyGesture(Key.O, ModifierKeys.Control), OpenOnExecute);//_Open...
gests.Add(new KeyGesture(Key.S, ModifierKeys.Control), SaveOnExecute);//_Save
gests.Add(new KeyGesture(Key.F2, ModifierKeys.Control), PrintPreviewOnExecute);//P_rint Preview
gests.Add(new KeyGesture(Key.P, ModifierKeys.Control), PrintOnExecute);//_Print...
// Edit
gests.Add(new KeyGesture(Key.Z, ModifierKeys.Control), UndoOnExecute);//_Undo
gests.Add(new KeyGesture(Key.Y, ModifierKeys.Control), RedoOnExecute);//_Redo
gests.Add(new KeyGesture(Key.X, ModifierKeys.Control), CutOnExecute);//Cu_t
gests.Add(new KeyGesture(Key.C, ModifierKeys.Control), CopyOnExecute);//_Copy
gests.Add(new KeyGesture(Key.V, ModifierKeys.Control), PasteOnExecute);//_Paste
gests.Add(new KeyGesture(Key.Delete, ModifierKeys.None), DeleteOnExecute);//De_lete
gests.Add(new KeyGesture(Key.F, ModifierKeys.Control), FindOnExecute);//_Find...
gests.Add(new KeyGesture(Key.F3, ModifierKeys.None), FindNextOnExecute);//Find _Next
gests.Add(new KeyGesture(Key.H, ModifierKeys.Control), ReplaceOnExecute);//_Replace...
gests.Add(new KeyGesture(Key.G, ModifierKeys.Control), GoToOnExecute);//_Go To...
gests.Add(new KeyGesture(Key.A, ModifierKeys.Control), SelectAllOnExecute);//Select _All
// Format
gests.Add(new KeyGesture(Key.W, ModifierKeys.Control), WordWrapOnExecute);//_Word Wrap
}
// Перекрываем стандартный обработчик
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
base.OnPreviewKeyDown(e);
// Ищем жест, останавливаем событие и исполняем обработчик
foreach (KeyGesture gest in gests.Keys)
if (gest.Matches(null, e)) // Сравниваем перехваченный жест с заданным в объекте
{
gests[gest](this, e); // Вызываем обработчик через словарь
e.Handled = true; // Останавливаем событие
break; // Прерываем цикл
}
}
}
}
Добавьте в конструктор
класса Window1 файла Window1.xaml.cs вызов нашей функции создания жестовpublic Window1()
{
InitializeComponent();
// Создание жестов
this.CreateGestures();
}
Запустите приложение
и убедитесь, что клавиатурные жесты работаютОдной из важных задач управления пользовательским интерфейсом является своевременное отключение источников команд, когда выполнение команды может противоречить логике работы приложения. Например, если в редакторе текста буфер обмена пуст, команду вставки следует сделать недоступной, или, если в загруженном файле не было изменений, то кнопку сохранения тоже нужно отключить.
Для нашего приложения проведем ревизию правил, по которым элементы интерфейса должны менять свое состояние в зависимости от возможности выполнения тех или иных задач на текущий момент.
Раздел File:
New: должна быть доступна всегда, поскольку пользователь
может захотеть в любой момент создать новый документ. Если в текущем документе
есть несохраненные изменения, то нужно вывести диалоговое окно с предложением
их сохранить, проигнорировать или отменить задачу. При утвердительном ответе
пользователя следует проверить, если новый документ сохраняется впервые,
то нужно предоставить диалоговое окно записи. Реализована!Open: аналогична задаче New, только после решения о сохранении
текущих изменений следует предоставить диалог открытия файла. Реализована!Save: при новом документе предоставить диалог записи. Если
документ уже сохранялся и имеет имя, но текущих изменений нет, то задачу
следует сделать недоступной, отключив соответствующие источники. При первом
же изменении нужно немедленно освободить источники выполнения этой задачи. Нереализована!Save As: должна быть доступна всегда. При запросе выполнения
этой задачи сразу предоставить пользователю диалог сохранения файла. Реализована!Page Setup, Print Preview, Print:
должны быть доступны всегдаExit: должна быть доступна всегда. Если есть несохраненные
изменения, частично выполнить задачу New и завершить приложение (возможно,
с сохранением текущего состояния приложения в ресурсном файле). Реализована
частично!Раздел Edit:
Undo, Redo: делать недоступными, когда восстанавливать
нечего. Нереализована!Cut, Copy: делать недоступными, если нет выделения
текста. Нереализована!Paste: делать недоступной, если буфер обмена пуст или в
нем сохранена нетекстовая информация. Нереализована!Delete: делать недоступной, если нет выделения текста. Нереализована!Find, Find Next, Replace, Go To:
доступны всегдаSelect All: недоступна в случае, если текстовое поле пустое. Нереализована!Раздел Format:
Font, Word Wrap: доступны всегдаРаздел Help:
About: доступна всегдаЧтобы продемонстрировать трудность реализации логики отключения источников,
ограничимся только одной задачей Save. В последующем упражнении, где будет
использован механизм команд, все решится гораздо проще. А пока только одна
задача - Save, чтобы зря не тратить силы.
Не хочется вмешиваться в ранее разработанный код, поскольку логику отключения мы оставили на потом и сейчас это может повлечь ошибки. Поэтому, наиболее разумно, добавить автономный код, не меняя прежнего, и разместить его в отдельном файле.
Выделите узел текущего
проекта и командой Project/Add Class добавьте новый файл с именем EnabledControls.cs,
который заполните такusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Controls.Primitives;// Для ButtonBase
namespace Notepad1
{
partial class Window1
{
}
}
Первое, что приходит
на ум - использовать событие texBox1.TextChanged, в котором проверять состояние
флага IsModified и принимать решение о недоступности или доступности источников
задачи Save.
Добавьте в файл EnabledControls.cs следующий
код, регистрирующий еще один обработчик события texBox1.TextChangednamespace Notepad1
{
partial class Window1
{
// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
// Еще один обработчик
// обычного события TextChanged
txtBox1.TextChanged += EnabledControls_TextChanged;
}
private void EnabledControls_TextChanged(object sender,
System.Windows.Controls.TextChangedEventArgs e)
{
// Изменяем состояние интерфейсных элементов _Save
itemSave.IsEnabled = btnSave.IsEnabled = IsModified;
}
}
}
В файле Window1.xaml.cs добавьте в конструктор класса Window1 последней строкой вызов функции AdditionalHandlers() такpublic Window1()
{
InitializeComponent();
// Создание жестов
this.CreateGestures();
// Дополнительные обработчики в файле EnabledControls.cs
AdditionalHandlers();
}
Запустите приложение
- до первого изменения текста кнопки источники Save блокированы, а потом
все работает не так. И жест Ctrl+S тоже доступен.Дело здесь в том, что событие TextChanged срабатывает раньше, чем будет установлен
флаг IsModified. Поэтому нужно обрабатывать не событие изменения текста, а
событие изменения флага IsModified. Следующим шагом мы преобразуем поле IsModified в свойство на базе нового логического поля modified и создадим свое событие,
в обработчике которого и решим управление доступностью источников задачи Save.
В файле Window1.xaml.cs найдите объявление поля IsModified и переименуйте его в modifiedБыло bool IsModified = false; // Флаг изменений содержимого Стало bool modified = false; // Флаг изменений содержимого
Поле modified будет базовым для свойства IsModified. Это все изменения, которые
мы вынуждены были провести в прежнем коде. Остальные изменения будем вносить
в файл EnabledControls.cs.
В файле EnabledControls.cs удалите весь код, связанный с событием texBox1.TextChanged и его обработчикомnamespace Notepad1
{
partial class Window1
{
// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
// Еще один обработчик
// обычного события TextChanged
txtBox1.TextChanged += EnabledControls_TextChanged;
}
private void EnabledControls_TextChanged(object sender,
System.Windows.Controls.TextChangedEventArgs e)
{
// Изменяем состояние интерфейсных элементов _Save
itemSave.IsEnabled = btnSave.IsEnabled = IsModified;
}
}
}
Добавьте в файл EnabledControls.cs новый код, чтобы файл стал такимusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Controls.Primitives;// Для ButtonBase
namespace Notepad1
{
partial class Window1
{
// Объявляем внутреннее событие
private event EventHandler ChangeModifiedEvent;
// Упаковываем базовое поле modified в свойство
private bool IsModified
{
get { return modified; }
set
{
if (modified != value)
{
modified = !modified;
// Инициируем событие, если есть обработчик
if (ChangeModifiedEvent != null)
ChangeModifiedEvent(this, EventArgs.Empty);
}
}
}
// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
// Начальные запрещения для _Save
itemSave.IsEnabled = btnSave.IsEnabled = false;
// Удаляем созданный в CreateGestures() жест _Save
foreach (KeyGesture gest in gests.Keys)
if (gests[gest] == SaveOnExecute)
{
gests.Remove(gest);
break;
}
// Регистрируем обработчик изменения свойства
this.ChangeModifiedEvent += Window1_ChangeModifiedEvent;
}
void Window1_ChangeModifiedEvent(object sender, EventArgs e)
{
//MessageBox.Show("Modify");
// Проверяем состояние любого из источников _Save
if (btnSave.IsEnabled == false)
// Добавляем жест _Save
gests.Add(new KeyGesture(Key.S, ModifierKeys.Control),
SaveOnExecute);//_Save
else
// Удаляем жест _Save
foreach (KeyGesture gest in gests.Keys)
if (gests[gest] == SaveOnExecute)
{
gests.Remove(gest);
break;
}
// Изменяем состояние интерфейсных элементов _Save
itemSave.IsEnabled = btnSave.IsEnabled = IsModified;
}
}
}
Запустите приложение
и убедитесь, что управление источниками задачи Save, включая жесты, во всех
режимах работает как и положено. Разберитесь с кодом!!!Для управления доступностью других задач приложения нужно построить что-то
подобное. Мы этого здесь делать не будем, однако и сейчас уже ясно, что это
непростая задача. Для желающих продолжить управление отключениями источников
можно посоветовать дополнить файл EnabledControls.cs новыми заготовками так
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Controls.Primitives;// Для ButtonBase
namespace Notepad1
{
partial class Window1
{
// Объявляем внутреннее событие
private event EventHandler ChangeModifiedEvent;
// Упаковываем базовое поле modified в свойство
private bool IsModified
{
get { return modified; }
set
{
if (modified != value)
{
modified = !modified;
// Инициируем событие, если есть обработчик
if (ChangeModifiedEvent != null)
ChangeModifiedEvent(this, EventArgs.Empty);
}
}
}
// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
// Регистрируем один и тот же обработчик
// всплывающих событий кнопок, элементов меню,
// клавиатурных жестов для окна
this.AddHandler(ButtonBase.ClickEvent,
new RoutedEventHandler(this.Window1_ButtonClick));
this.AddHandler(MenuItem.ClickEvent,
new RoutedEventHandler(this.Window1_ItemClick));
this.AddHandler(Keyboard.KeyDownEvent,
new RoutedEventHandler(this.Window1_Gesture));
// Дополнительный общий обработчик элементов контекстного меню
contextCut.Click += new RoutedEventHandler(item_Context);
contextCopy.Click += new RoutedEventHandler(item_Context);
contextPaste.Click += item_Context; // Упрощенный синтаксис
contextDelete.Click += item_Context;
// Начальные запрещения для _Save
itemSave.IsEnabled = btnSave.IsEnabled = false;
// Удаляем созданный в CreateGestures() жест _Save
foreach (KeyGesture gest in gests.Keys)
if (gests[gest] == SaveOnExecute)
{
gests.Remove(gest);
break;
}
// Регистрируем обработчик изменения свойства
this.ChangeModifiedEvent += Window1_ChangeModifiedEvent;
}
void Window1_ChangeModifiedEvent(object sender, EventArgs e)
{
//MessageBox.Show("Modify");
// Проверяем состояние любого из источников _Save
if (btnSave.IsEnabled == false)
// Добавляем жест _Save
gests.Add(new KeyGesture(Key.S, ModifierKeys.Control),
SaveOnExecute);//_Save
else
// Удаляем жест _Save
foreach (KeyGesture gest in gests.Keys)
if (gests[gest] == SaveOnExecute)
{
gests.Remove(gest);
break;
}
// Изменяем состояние интерфейсных элементов _Save
itemSave.IsEnabled = btnSave.IsEnabled = IsModified;
}
private void Window1_ButtonClick(object sender, RoutedEventArgs e)
{
//MessageBox.Show("Button");
// Повышаем полномочия ссылки
Button btn = sender as Button;
if (btn == btnSave)
{
;
}
}
private void Window1_ItemClick(object sender, RoutedEventArgs e)
{
//MessageBox.Show("Item");
// Повышаем полномочия ссылки
MenuItem item = sender as MenuItem;
if (item == itemSave)
{
;
}
}
private void Window1_Gesture(object sender, RoutedEventArgs e)
{
//MessageBox.Show("Key");
}
private void item_Context(object sender, RoutedEventArgs e)
{
//MessageBox.Show("Context");
}
}
}
Добавленный код пока ни на что не влияет, но может стать отправной точкой
для дальнейших действий по блокированию других задач. В следующим упражнении
мы все решим гораздо проще, используя встроенный в WPF механизм команд.
Мы уже столько потрудились над этим блокнотом, который так наивно назвали простым, что неразумно будет начинать все заново. Проще скопировать полученный проект в новый и там проводить все необходимые изменения. Но трудились мы не для сего блокнота, а ради будущих наших профессиональных успехов (Se La Vi - такова жизнь).
Зайдите в проводнике Windows Explorer в каталог решения EventsAndCommands и скопируйте папку Notepad1
Переименуйте копию
проекта в Notepad2
Зайдите внутрь папки Notepad2 и переименуйте файл проекта тоже в Notepad2. Здесь
же удалите каталоги obj и bin, которые оболочка вновь создаст при запуске приложения
Запустите оболочку,
выделите EventsAndCommands и командой File/Add/Existing Project
зарегистрируйте в решении оболочки проект Notepad2
Не запуская проект,
откройте на редактирование любой файл проекта Notepad2, вызовите жестом Ctrl+H окно замены с указанными на рисунке настройками и сделайте замену по
всему проекту
Должно получиться 20 замен.
В панели Solution
Explorer вызовите контекстное меню для узла проекта Notepad2 и командой Set as StartUp Project назначьте его стартовым
Запустите приложение Notepad2 и убедитесь, что все работает как в проекте Notepad1Теперь будем модифицировать этот проект, подгоняя его под использование механизма
команд. Но прежде проведем ревизию библиотечных команд, возможно не все из
них закрывают наши источники и часть команд придется создавать самим. За
основу возьмем источники меню, поскольку там наиболее полно представлены
задачи нашего приложения. Интересующие нас библиотечные команды находятся
в классе ApplicationCommands пространства имен System.Windows.Input
| Раздел меню | Задача | Библиотечная команда | Встроенные жесты | Наши жесты | Регулировать доступность ? |
|---|---|---|---|---|---|
| File | New | ApplicationCommands.New | Ctrl+N | Ctrl+N | |
| Open | ApplicationCommands.Open | Ctrl+O | Ctrl+O | ||
| Save | ApplicationCommands.Save | Ctrl+S | Ctrl+S | Да | |
| Save As | ApplicationCommands.SaveAs | ||||
| Page Setup | |||||
| Print Preview | ApplicationCommands.PrintPreview | Ctrl+F2 | Ctrl+F2 | ||
| ApplicationCommands.Print | Ctrl+P | Ctrl+P | |||
| Exit | |||||
| Edit | Undo | ApplicationCommands.Undo | Ctrl+Z | Ctrl+Z | Да |
| Redo | ApplicationCommands.Redo | Ctrl+Y | Ctrl+Y | Да | |
| Cut | ApplicationCommands.Cut | Ctrl+X | Ctrl+X | Да | |
| Copy | ApplicationCommands.Copy | Ctrl+C | Ctrl+C | Да | |
| Paste | ApplicationCommands.Paste | Ctrl+V | Ctrl+V | Да | |
| Delete | ApplicationCommands.Delete | Del | Del | Да | |
| Find | ApplicationCommands.Find | Ctrl+F | Ctrl+F | ||
| Find Next | F3 | ||||
| Replace | ApplicationCommands.Replace | Ctrl+H | Ctrl+H | Да | |
| Go To | Ctrl+G | ||||
| Select All | ApplicationCommands.SelectAll | Ctrl+A | Ctrl+A | Да | |
| Format | Font | ||||
| Word Wrap | Ctrl+W | ||||
| Help | About |
Глядя на таблицу, можно сделать следующие выводы для нашего приложения:
Недостающие команды нужно создавать в процедурном коде. А вот подключать
все команды к источникам и связывать с коллекцией CommandBindings окна, пять
же для тренировки, будем и в разметке, и в процедурном коде.
Еще раз выпишем те задачи, которые мы будем реализовывать с помощью команд
| Где присоединить к источнику? | Где привязать к окну? | Задача | Библиотечная команда | Alias (псевдоним) | Жесты | Рег? |
|---|---|---|---|---|---|---|
| Разметка | Разметка | Save | Save | SaveCommand | Ctrl+S | Да |
| Разметка | Разметка | Page Setup | PageSetupCommand | Нет | ||
| Разметка | Разметка | Undo | Undo | UndoCommand | Ctrl+Z | Да |
| Разметка | Код | Redo | Redo | RedoCommand | Ctrl+Y | Да |
| Разметка | Код | Cut | Cut | CutCommand | Ctrl+X | Да |
| Разметка | Код | Copy | Copy | CopyCommand | Ctrl+C | Да |
| Код | Разметка | Paste | Paste | PasteCommand | Ctrl+V | Да |
| Код | Разметка | Delete | Delete | DeleteCommand | Del | Да |
| Код | Разметка | Find Next | FindNextCommand | F3 |
Да | |
| Код | Код | Replace | Replace | ReplaceCommand | Ctrl+H | Да |
| Код | Код | Go To | GoToCommand | Ctrl+G |
Нет | |
| Код | Код | Select All | SelectAll | SelectAllCommand | Ctrl+A | Да |
Мы выбрали 12 задач, которые хотим реализовать с помощью команд. Эта табличка будет нашим планом для дальнейшей работы.
Откройте файл Window1.xaml.cs текущего
проекта Notepad2, найдите объявление поля modified и переименуйте
его вновь в IsModified такБыло bool modified = false; // Флаг изменений содержимого Стало bool IsModified = false; // Флаг изменений содержимого
Откройте файл EnabledControls.cs,
уберите в нем лишний код, чтобы остался только такойusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad2
{
partial class Window1
{
// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
}
}
}
Внесите в файл EnabledControls.cs код определения и инициализации команд вместе со встроенными
жестамиnamespace Notepad2
{
partial class Window1
{
// Объявляем и инициализируем поля команд
// Их обязательно нужно объявлять статическими, чтобы
// размещались в объекте-типе и уже были созданы перед
// созданием элементов, к которым присоединяются
// Последний параметр означает жесты
public static RoutedCommand SaveCommand = ApplicationCommands.Save;
public static RoutedCommand PageSetupCommand =
new RoutedCommand("PageSetup", typeof(Window1), null);// Без жеста
public static RoutedCommand UndoCommand = ApplicationCommands.Undo;
public static RoutedCommand RedoCommand = ApplicationCommands.Redo;
public static RoutedCommand CutCommand = ApplicationCommands.Cut;
public static RoutedCommand CopyCommand = ApplicationCommands.Copy;
public static RoutedCommand PasteCommand = ApplicationCommands.Paste;
public static RoutedCommand DeleteCommand = ApplicationCommands.Delete;
public static RoutedCommand FindNextCommand;// Определим в ст. конструкторе
public static RoutedCommand ReplaceCommand = ApplicationCommands.Replace;
public static RoutedCommand GoToCommand;// Определим в ст. конструкторе
public static RoutedCommand SelectAllCommand = ApplicationCommands.SelectAll;
// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
}
}
}
Мы объявили псевдонимы команд как общедоступные поля класса Window1. Команды,
которые имеют встроенные жесты или не должны иметь жестов, мы инициализировали
сразу. Две команды только объявили, но сами объекты собираемся создать в коде.
Код создания этих команд мы поместим в статический конструктор
для добавления жестов. Это нужно для того, что добавление жестов требует действий,
а это разрешено только в методах. Конструктор должен быть обязательно статическим,
чтобы мог выполниться до создания экземпляра окна. Все команды должны к этому
времени уже существовать, поскольку используются в разметке окна при создании
интерфейсных элементов.
Добавьте в класс Window1 файла EnabledControls.cs статический конструктор со следующим кодомnamespace Notepad2
{
partial class Window1
{
..............................................
// Статический конструктор
static Window1()
{
// Определяем с добавлением жестов
InputGestureCollection coll = new InputGestureCollection();
coll.Add(new KeyGesture(Key.F3, ModifierKeys.None, "F3"));
FindNextCommand = new RoutedCommand("FindNext", typeof(Window1), coll);
coll = new InputGestureCollection();
coll.Add(new KeyGesture(Key.G, ModifierKeys.Control, "Ctrl+G"));
GoToCommand = new RoutedCommand("GoTo", typeof(Window1), coll);
}
// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
}
}
}
Не забывайте, что статический конструктор класса в C# не принимает параметров
и может существовать только в единственном экземпляре (если объявим).
Теперь в двух пользовательских командах имеются как жесты, так и вся необходимая информация для отображения в пунктах меню.
В соответствии с планом, присоединим команды к источникам, вначале в коде.
Добавьте в функцию AdditionalHandlers() файла EnabledControls.cs следующий кодvoid AdditionalHandlers()
{
Clipboard.Clear();// Временно, чтобы испытать начальное состояние
// Присоединяем команды к источникам, жесты уже встроены в команды
//SaveCommand - присоединим в разметке
//PageSetupCommand - присоединим в разметке
//UndoCommand - присоединим в разметке
//RedoCommand - присоединим в разметке
//CutCommand - присоединим в разметке
//CopyCommand - присоединим в разметке
btnPaste.Command = itemPaste.Command = contextPaste.Command = PasteCommand;
btnDelete.Command = itemDelete.Command = contextDelete.Command = DeleteCommand;
itemFindNext.Command = FindNextCommand;
itemReplace.Command = ReplaceCommand;
itemGoTo.Command = GoToCommand;
itemSelectAll.Command = SelectAllCommand;
}
Добавьте в открывающем
дескрипторе <Window> файла Window1.xaml параметр отображения пространства
имен класса процедурного кода на разметку, чтобы компилятор видел вставляемые
в разметку команды. Имя отображения можно принять произвольно - выберите myCmdКогда вы начнете вручную набирать запись xmlns:myCmd=,
то после ввода знака присваивания IntelliSense выдаст подсказку, в которой
нужно выбрать выделенную на снимке опцию списка

В результате будет догенерирована следующая запись
<Window x:Class="Notepad2.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:myCmd="clr-namespace:Notepad2"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
Closing="Window_Closing"
Activated="Window_Activated"
>
..............................................
</Window>
Теперь отредактируем разметку 12 запланированных источников задач в соответствии с таблицей
| Где присоединить к источнику? | Задача | Alias (псевдоним) |
|---|---|---|
| Разметка | Save | SaveCommand |
| Разметка | Page Setup | PageSetupCommand |
| Разметка | Undo | UndoCommand |
| Разметка | Redo | RedoCommand |
| Разметка | Cut | CutCommand |
| Разметка | Copy | CopyCommand |
| Paste | PasteCommand | |
| Delete | DeleteCommand | |
| Find Next | FindNextCommand | |
| Replace | ReplaceCommand | |
| Go To | GoToCommand | |
| Select All | SelectAllCommand |
В файле Window1.xaml текущего проекта удалите в источниках для первых 6 задач таблицы
тексты жестов и замените записи с событием Click на записи с присоединением
команд
В файле Window1.xaml текущего проекта удалите в источниках для последних 6 задач таблицы
тексты жестов и записи события Click. Для этих источников присоединение
команд мы уже выполнили в процедурном кодеПравленный код разметки станет таким (для удобства файл Window1.xaml с новым
содержимым приводится полностью)
<Window x:Class="Notepad2.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:myCmd="clr-namespace:Notepad2"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
Closing="Window_Closing"
Activated="Window_Activated"
>
<Window.Resources>
<!-- File -->
<Image x:Shared="False" x:Key="iconNew" Source="Images/NewDocumentHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconOpen" Source="Images/OpenHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconSave" Source="Images/SaveHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPageSetup" Source="Images/PrintSetupHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPrintPreview" Source="Images/PrintPreviewHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPrint" Source="Images/PrintHS.png" Width="16" Height="16" />
<!-- Edit -->
<Image x:Shared="False" x:Key="iconUndo" Source="Images/Edit_UndoHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconRedo" Source="Images/Edit_RedoHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconCut" Source="Images/CutHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconCopy" Source="Images/CopyHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPaste" Source="Images/PasteHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconDelete" Source="Images/DeleteHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconFind" Source="Images/FindHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconFont" Source="Images/FontHS.png" Width="16" Height="16" />
</Window.Resources>
<DockPanel LastChildFill="True">
<!-- Меню -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<!-- Сокращенные варианты подключения иконок с использованием статических ресурсов -->
<MenuItem Name="itemNew" Click="NewOnExecute" Header="_New" InputGestureText="Ctrl+N"
Icon="{StaticResource iconNew}" />
<MenuItem Name="itemOpen" Click="OpenOnExecute" Header="_Open..." InputGestureText="Ctrl+O"
Icon="{StaticResource iconOpen}" />
<MenuItem Name="itemSave" Command="myCmd:Window1.SaveCommand" Header="_Save"
Icon="{StaticResource iconSave}" />
<MenuItem Name="itemSaveAs" Click="SaveAsOnExecute" Header="Save _As..." />
<Separator />
<MenuItem Name="itemPageSetup" Command="myCmd:Window1.PageSetupCommand" Header="Page Set_up..."
Icon="{StaticResource iconPageSetup}" />
<MenuItem Name="itemPrintPreview" Click="PrintPreviewOnExecute" Header="P_rint Preview"
InputGestureText="Ctrl+F2" Icon="{StaticResource iconPrintPreview}" />
<MenuItem Name="itemPrint" Click="PrintOnExecute" Header="_Print..."
InputGestureText="Ctrl+P" Icon="{StaticResource iconPrint}" />
<Separator />
<MenuItem Name="itemExit" Click="ExitOnExecute" Header="E_xit" />
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Name="itemUndo" Command="myCmd:Window1.UndoCommand" Header="_Undo" Icon="{StaticResource iconUndo}" />
<MenuItem Name="itemRedo" Command="myCmd:Window1.RedoCommand" Header="_Redo" Icon="{StaticResource iconRedo}" />
<Separator></Separator>
<MenuItem Name="itemCut" Command="myCmd:Window1.CutCommand" Header="Cu_t" Icon="{StaticResource iconCut}" />
<MenuItem Name="itemCopy" Command="myCmd:Window1.CopyCommand" Header="_Copy" Icon="{StaticResource iconCopy}" />
<MenuItem Name="itemPaste" Header="_Paste" Icon="{StaticResource iconPaste}" />
<MenuItem Name="itemDelete" Header="De_lete" Icon="{StaticResource iconDelete}" />
<Separator></Separator>
<MenuItem Name="itemFind" Click="FindOnExecute" Header="_Find..." InputGestureText="Ctrl+F"
Icon="{StaticResource iconFind}" />
<MenuItem Name="itemFindNext" Header="Find _Next" />
<MenuItem Name="itemReplace" Header="_Replace..." />
<MenuItem Name="itemGoTo" Header="_Go To..." />
<Separator></Separator>
<MenuItem Name="itemSelectAll" Header="Select _All" />
</MenuItem>
<MenuItem Header="F_ormat">
<MenuItem Name="itemFont" Click="FontOnExecute" Header="_Font..." Icon="{StaticResource iconFont}" />
<Separator />
<MenuItem Name="itemWordWrap" Click="WordWrapOnExecute" Header="_Word Wrap" IsCheckable="True"
IsChecked="True" InputGestureText="Ctrl+W" />
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Name="itemAbout" Click="AboutOnExecute" Header="_About" />
</MenuItem>
</Menu>
<!-- Панель инструментов -->
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<Button Name="btnNew" Click="NewOnExecute" Width="23" Content="{StaticResource iconNew}" />
<Button Name="btnOpen" Click="OpenOnExecute" Width="23" Content="{StaticResource iconOpen}" />
<Button Name="btnSave" Command="myCmd:Window1.SaveCommand" Width="23" Content="{StaticResource iconSave}" />
</ToolBar>
<ToolBar>
<Button Name="btnUndo" Command="myCmd:Window1.UndoCommand" Width="23" Content="{StaticResource iconUndo}" />
<Button Name="btnRedo" Command="myCmd:Window1.RedoCommand" Width="23" Content="{StaticResource iconRedo}" />
<Separator />
<Button Name="btnCut" Command="myCmd:Window1.CutCommand" Width="23" Content="{StaticResource iconCut}" />
<Button Name="btnCopy" Command="myCmd:Window1.CopyCommand" Width="23" Content="{StaticResource iconCopy}" />
<Button Name="btnPaste" Width="23" Content="{StaticResource iconPaste}" />
<Button Name="btnDelete" Width="23" Content="{StaticResource iconDelete}" />
</ToolBar>
<ToolBar Header="Find:">
<TextBox Width="100" />
<Button Name="btnFind" Click="FindOnExecute" Width="23" Content="{StaticResource iconFind}" />
</ToolBar>
</ToolBarTray>
<!-- Строка состояния -->
<StatusBar DockPanel.Dock="Bottom" Height="32" Name="statusBar">
<Label>Simulator Application is Loading</Label>
<Separator />
<ProgressBar Height="20" Width="100" IsIndeterminate="True" />
</StatusBar>
<!-- Многострочное текстовое поле редактирования -->
<TextBox TextWrapping="Wrap"
AcceptsReturn="True"
AcceptsTab="True"
VerticalScrollBarVisibility="Auto"
Name="txtBox1"
TextChanged="txtBox1_TextChanged"
HorizontalScrollBarVisibility="Auto"
>
<TextBox.ContextMenu>
<ContextMenu Width="100">
<MenuItem Name="contextCut" Command="myCmd:Window1.CutCommand" Header="Cu_t" Icon="{StaticResource iconCut}" />
<MenuItem Name="contextCopy" Command="myCmd:Window1.CopyCommand" Header="_Copy" Icon="{StaticResource iconCopy}"/>
<MenuItem Name="contextPaste" Header="_Paste" Icon="{StaticResource iconPaste}" />
<MenuItem Name="contextDelete" Header="De_lete" Icon="{StaticResource iconDelete}" />
</ContextMenu>
</TextBox.ContextMenu>
</TextBox>
</DockPanel>
</Window>
Эта разметка визуально получилась достаточно широкой, ее лучше прежде скопировать, а потом разбирать.
Удалите (я закомментировал)
код создания жестов в функции CreateGestures() файла KeyGestures.cs для
выбранных нами 12 задач (для задачи Page Setup жестов нет)void CreateGestures()
{
// File
gests.Add(new KeyGesture(Key.N, ModifierKeys.Control), NewOnExecute);//_New
gests.Add(new KeyGesture(Key.O, ModifierKeys.Control), OpenOnExecute);//_Open...
//gests.Add(new KeyGesture(Key.S, ModifierKeys.Control), SaveOnExecute);//_Save
gests.Add(new KeyGesture(Key.F2, ModifierKeys.Control), PrintPreviewOnExecute);//P_rint Preview
gests.Add(new KeyGesture(Key.P, ModifierKeys.Control), PrintOnExecute);//_Print...
// Edit
//gests.Add(new KeyGesture(Key.Z, ModifierKeys.Control), UndoOnExecute);//_Undo
//gests.Add(new KeyGesture(Key.Y, ModifierKeys.Control), RedoOnExecute);//_Redo
//gests.Add(new KeyGesture(Key.X, ModifierKeys.Control), CutOnExecute);//Cu_t
//gests.Add(new KeyGesture(Key.C, ModifierKeys.Control), CopyOnExecute);//_Copy
//gests.Add(new KeyGesture(Key.V, ModifierKeys.Control), PasteOnExecute);//_Paste
//gests.Add(new KeyGesture(Key.Delete, ModifierKeys.None), DeleteOnExecute);//De_lete
gests.Add(new KeyGesture(Key.F, ModifierKeys.Control), FindOnExecute);//_Find...
//gests.Add(new KeyGesture(Key.F3, ModifierKeys.None), FindNextOnExecute);//Find _Next
//gests.Add(new KeyGesture(Key.H, ModifierKeys.Control), ReplaceOnExecute);//_Replace...
//gests.Add(new KeyGesture(Key.G, ModifierKeys.Control), GoToOnExecute);//_Go To...
//gests.Add(new KeyGesture(Key.A, ModifierKeys.Control), SelectAllOnExecute);//Select _All
// Format
gests.Add(new KeyGesture(Key.W, ModifierKeys.Control), WordWrapOnExecute);//_Word Wrap
}
Запустите приложение
и убедитесь, что тексты жестов во всех источниках команд присутствуют
несмотря на то, что мы их только что явно удалили. Теперь жесты в источники
попадают из командВ начальном состоянии источники команд являются недоступными, поскольку пока
нет привязки к элементам визуального дерева и обработчикам. Исправим это и
привяжем команды к коллекции CommandBindings окна Window1. Причем, в соответствии
с намеченным планом выполним привязку одной части команд в коде, а остальной
- в разметке.
В месте привязки для каждого объекта привязки нужно указать имя команды,
имя обработчика события Executed и имя обработчика события CanExecute для
управления доступностью источников. Оба события всплывающие, поэтому местом
привязки можно выбрать любой элемент маршрута, например, окно Window1. Это
корень визуального дерева и ни одно всплывающее событие мимо него не пройдет.
Обработчики
события Executed для выполнения команд у нас есть, мы их уже создавали для
обработки события Click. Например, для команды Save обработчик имеет имя SaveOnExecute и его сигнатура выглядит так
private void SaveOnExecute(object sender, RoutedEventArgs e)
{
}
Нам предстоит создать обработчики события CanExecute и мы разместим их в файле EnabledControls.cs. Они будут иметь несколько
иную сигнатуру. Например, для команды Save такой обработчик должен иметь следующую
заготовку
private void SaveCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
Добавьте в класс Window1 файла EnabledControls.cs заготовки обработчиков события CanExecute для выбранных
нами ранее 12 команд (число-то какое хорошее!)// Обработчики события CanExecute команд
private void SaveCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void PageSetupCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void UndoCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void RedoCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void CutCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void CopyCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void PasteCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void DeleteCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void FindNextCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void ReplaceCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void GoToCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void SelectAllCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
Теперь пришла пора выполнить саму привязку в соответствии с намеченным планом
| Где привязать к окну? | Задача | Alias (псевдоним) | Жесты | Регулировать доступность источников? |
|---|---|---|---|---|
| Разметка | Save | SaveCommand | Ctrl+S | Да |
| Разметка | Page Setup | PageSetupCommand | Нет |
|
| Разметка | Undo | UndoCommand | Ctrl+Z | Да |
| Код | Redo | RedoCommand | Ctrl+Y | Да |
| Код | Cut | CutCommand | Ctrl+X | Да |
| Код | Copy | CopyCommand | Ctrl+C | Да |
| Разметка | Paste | PasteCommand | Ctrl+V | Да |
| Разметка | Delete | DeleteCommand | Del | Да |
| Разметка | Find Next | FindNextCommand | F3 | Да |
| Код | Replace | ReplaceCommand | Ctrl+H | Да |
| Код | Go To | GoToCommand | Ctrl+G | Нет |
| Код | Select All | SelectAllCommand | Ctrl+A | Да |
Обратите внимание, что источники команд PageSetupCommand и GoToCommand должны быть доступны всегда, поэтому привяжем для них только обработчики события Executed
Удалите в файле EnabledControls.cs заготовки обработчиков события CanExecute для
команд PageSetupCommand и GoToCommand (или не удаляйте и пусть
болтаются как незадействованные методы)
В файле Window1.xaml текущего проекта выполните привязку части команд к объекту окна, для этого
после открывающего дескриптора окна вставьте следующую разметку<Window x:Class="Notepad2.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:myCmd="clr-namespace:Notepad2"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
Closing="Window_Closing"
Activated="Window_Activated"
>
<!-- Привязка команд к объекту окна -->
<Window.CommandBindings>
<CommandBinding Command="myCmd:Window1.SaveCommand" Executed="SaveOnExecute" CanExecute="SaveCanExecute" />
<CommandBinding Command="myCmd:Window1.PageSetupCommand" Executed="PageSetupOnExecute" />
<CommandBinding Command="myCmd:Window1.UndoCommand" Executed="UndoOnExecute" CanExecute="UndoCanExecute" />
<CommandBinding Command="myCmd:Window1.PasteCommand" Executed="PasteOnExecute" CanExecute="PasteCanExecute" />
<CommandBinding Command="myCmd:Window1.DeleteCommand" Executed="DeleteOnExecute" CanExecute="DeleteCanExecute" />
<CommandBinding Command="myCmd:Window1.FindNextCommand" Executed="FindNextOnExecute" CanExecute="FindNextCanExecute" />
</Window.CommandBindings>
..........................................
</Window>
В файле EnabledControls.cs добавьте в функцию AdditionalHandlers() код привязки части команд к объекту
окна, после чего функция должна стать такой// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
Clipboard.Clear();// Временно, чтобы испытать начальное состояние
// Присоединяем команды к источникам, жесты уже встроены в команды
//SaveCommand - присоединим в разметке
//PageSetupCommand - присоединим в разметке
//UndoCommand - присоединим в разметке
//RedoCommand - присоединим в разметке
//CutCommand - присоединим в разметке
//CopyCommand - присоединим в разметке
btnPaste.Command = itemPaste.Command = contextPaste.Command = PasteCommand;
btnDelete.Command = itemDelete.Command = contextDelete.Command = DeleteCommand;
itemFindNext.Command = FindNextCommand;
itemReplace.Command = ReplaceCommand;
itemGoTo.Command = GoToCommand;
itemSelectAll.Command = SelectAllCommand;
// Привязка части команд к объекту окна в коде
// 0)
this.CommandBindings.Add(new CommandBinding(RedoCommand, RedoOnExecute, RedoCanExecute));
this.CommandBindings.Add(new CommandBinding(CutCommand, CutOnExecute, CutCanExecute));
this.CommandBindings.Add(new CommandBinding(CopyCommand, CopyOnExecute, CopyCanExecute));
// Теперь чуть подлиннее: создаем, настраиваем, привязываем!
// 1)
CommandBinding binding = new CommandBinding();
binding.Command = ReplaceCommand;
binding.Executed += ReplaceOnExecute;
binding.CanExecute += ReplaceCanExecute;
this.CommandBindings.Add(binding);
// 2)
binding = new CommandBinding(GoToCommand);
binding.Executed += GoToOnExecute;
this.CommandBindings.Add(binding);
// 3)
binding = new CommandBinding(SelectAllCommand, SelectAllOnExecute);
binding.CanExecute += SelectAllCanExecute;
this.CommandBindings.Add(binding);
}
После того, как ссылка binding, которая пока единствественная адресовала
объект, передаст адрес закрепленного за ней объекта в коллекцию CommandBindings элемента,
этот объект станет адресоваться в двух местах: в коллекции и в ссылке. Поэтому
такая ссылка станет уже излишней и ее можно будет использовать для присвоения
адреса нового объекта. А коллекция так и будет продолжать адресовать брошенный
ссылкой объект. В коде показано применение вариантов перегрузок конструктора
класса CommandBinding и
настройки свойств объекта.
Запустите приложение
и убедитесь в следующем...Источники, к которым мы присоединили команды, по прежнему остаются недоступными,
как после того, как мы к ним команды только присоединили. Но заметьте, что
два источника: File/Page Setup... и Edit/Go To... открылись и функционируют,
а для Go To даже клавиатурный жест Ctrl+G действует нормально. Это происходит
потому, что мы для них не зарегистрировали обработчики событий CanExecute и они свободно вызывают обработчики, зарегистрированные в событии Executed,
а остальные источники ждут разрешение на доступность. Создание таких разрешений
в обработчиках CanExecute и составляет суть реализации логики доступности
источников команд.
В соответствии с логикой
работы подобных приложений заполните заготовки обработчиков события CanExecute в файле EnabledControls.cs следующим образом (комментарии в коде)// Обработчики события CanExecute команд
private void SaveCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Позволить, если есть несохраненные изменения
e.CanExecute = IsModified;
}
/*
private void PageSetupCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Источник не регулируется
}
*/
private void UndoCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Позволить, если есть что откатывать
e.CanExecute = IsModified;
}
private void RedoCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Позволить, если очередь отмены есть и стоим не самые первые
e.CanExecute = txtBox1.CanRedo;
}
private void CutCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Позволить, если есть выделенный текст
e.CanExecute = txtBox1.SelectionLength > 0;
}
private void CopyCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Позволить, если есть выделенный текст
e.CanExecute = txtBox1.SelectionLength > 0;
}
private void PasteCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Позволить, если буфер обмена непустой и там содержится текстовый формат
e.CanExecute = Clipboard.ContainsText();
}
private void DeleteCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Позволить, если есть выделенный текст
e.CanExecute = txtBox1.SelectionLength > 0;
}
private void FindNextCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
//!!!!! От фонаря - не хочется думать !!!!!
e.CanExecute = txtBox1.CaretIndex < txtBox1.Text.Length;
}
private void ReplaceCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
//!!!!! От фонаря - не хочется думать !!!!!
e.CanExecute = txtBox1.CaretIndex < txtBox1.Text.Length;
}
/*
private void GoToCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Источник не регулируется
}
*/
private void SelectAllCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Заблокировать, если нечего выделять или уже все выделено
if (txtBox1.Text == String.Empty ||
txtBox1.SelectionLength == txtBox1.Text.Length)
e.CanExecute = false;
else
e.CanExecute = true;
}
Запустите приложение
и убедитесь... - что все работает не так как надо!!!Исправим это. Начнем с журнала откатов, встроенного в элемент TextBox.
По логике, очередь изменений должна очищаться при создании
нового документа или открытии существующего. А эта задача у
нас выполняется в обработчиках NewOnExecute() и OpenOnExecute().
Откройте файл File.cs и добавьте в упомянутые обработчики код очистки txtBox1 следующим образомprivate void NewOnExecute(object sender, RoutedEventArgs e)
{
// Пользователь передумал или была ошибка записи изменений
if (!CheckModifiedAndSaveIt())
return;
// Изменений нет или они успешно сохранены
//txtBox1.Text = String.Empty; // Вариант I
//txtBox1.Text = ""; // Вариант II
txtBox1.Clear(); // Вариант III
strLoadedFile = null;
IsModified = false;
UpdateTitle();
txtBox1.UndoLimit = 0; // Очистка очереди отмены
txtBox1.UndoLimit = -1; // Размер по умолчанию ограничен памятью
txtBox1.Focus();
// Надо отключить Cut, Copy и Delete
}
private void OpenOnExecute(object sender, RoutedEventArgs e)
{
if (DisplayOpenDialog())
{
txtBox1.CaretIndex = txtBox1.Text.Length;// Курсор в конец
txtBox1.UndoLimit = 0; // Очистка очереди отмены
txtBox1.UndoLimit = -1; // Размер по умолчанию ограничен памятью
}
txtBox1.Focus();// Передача фокуса
}
Откройте файл Edit.cs и добавьте в обработчик UndoOnExecute() следующий кодprivate void UndoOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Undo();
if (!txtBox1.CanUndo)
IsModified = false;
}
Запустите приложение
и убедитесь... - что опять все работает не так как надо!!!Вроде бы все предусмотрели, в чем здесь дело?
Проставьте в файле Edit.cs вдоль левого поля в теле обработчиков
UndoOnExecute(), RedoOnExecute(), CutOnExecute(), CopyOnExecute(),
PasteOnExecute(), DeleteOnExecute(), SelectAllOnExecute()
напротив исполнимого кода точки останова (Breakpoin), как показано на снимке
>
Запустите приложение
и поэксперименируйте с источниками команд - ни один
из отмеченных обработчиков (кроме DeleteOnExecute, да и тот на жест Del не реагирует) не срабатывает, хотя текстовый элемент свои функции выполняет
исправноЭто происходит из-за того, что для соответствующих задач мы использовали
библиотечные команды, которые напрямую работают с текстовыми элементами, имеющими
фокус ввода. Ну работают и пусть себе работают, поскольку в наших обработчиках
предусмотрена та же самая функциональность, кроме UndoOnExecute().
Удалите все точки
останова командой оболочки Debug/Delete All Breakpoints
Теперь в файле EnabledControls.cs проставляйте
поодиночке точки останова и каждый раз запускайте приложение для обработчиков
UndoCanExecute(), RedoCanExecute(), CutCanExecute(), CopyCanExecute(),
PasteCanExecute(), DeleteCanExecute(), SelectAllCanExecute(). Для остановки процесса пользуйтесь командой Stop Debugging меню Debug оболочки или одноименной кнопкой панели инструментов!!!Мы видим, что все эти обработчики события CanExecute срабатывают исправно
для всех команд: и библиотечных, и пользовательских. Причем срабатывают перед
тем, как элемент показывается при раскрытии меню, начальной отрисовке или
наведении курсора. То есть срабатывает именно тогда, когда нужно принять решение,
с какой доступностью этот элемент отобразить.
Итак, для правильной работы логики доступности источников команд нам нужно
заставить выполняться именно наши обработчики для команд Undo, Redo и SelectAll.
Добавьте в статический
конструктор файла EnabledControls.cs следующий код переопределения библиотечных
команд на пользовательские// Статический конструктор
static Window1()
{
// Определяем с добавлением жестов
InputGestureCollection coll = new InputGestureCollection();
coll.Add(new KeyGesture(Key.F3, ModifierKeys.None, "F3"));
FindNextCommand = new RoutedCommand("FindNext", typeof(Window1), coll);
coll = new InputGestureCollection();
coll.Add(new KeyGesture(Key.G, ModifierKeys.Control, "Ctrl+G"));
GoToCommand = new RoutedCommand("GoTo", typeof(Window1), coll);
// Заменяем библиотечные команды на свои для правильной работы логики
coll = new InputGestureCollection();
coll.Add(new KeyGesture(Key.Z, ModifierKeys.Control, "Ctrl+Z"));
UndoCommand = new RoutedCommand("Undo", typeof(Window1), coll);
coll = new InputGestureCollection();
coll.Add(new KeyGesture(Key.Y, ModifierKeys.Control, "Ctrl+Y"));
RedoCommand = new RoutedCommand("Redo", typeof(Window1), coll);
coll = new InputGestureCollection();
coll.Add(new KeyGesture(Key.A, ModifierKeys.Control, "Ctrl+A"));
SelectAllCommand = new RoutedCommand("SelectAll", typeof(Window1), coll);
}
Обратите внимание на то, что мы не удалили в полях инициализацию переопределенных ссылок библиотечными командами. Мы понадеялись на порядок инициализации полей и срабатывания статического конструктора. Вначале вынесенные в поля класса ссылки на команды объявляются и инициализируются, а затем в статическом конструкторе эти же ссылки получают адреса новых команд. Это малоприметный, но очень важный нюанс, проигнорировав который можно получить неуловимую ошибку выполнения.
Это плохая практика программирования, поскольку завтра могут выпустить другой
транслятор или среду исполнения CLR, где этот порядок будет изменен,
и наша программа станет работать неверно. Microsoft не дремлет (!!!) и регулярно посылает
по сети обновления и исправления. А что они завтра пришлют, кто его знает.
Поэтому лучше было бы в полях ссылки только объявить, а уже в конструкторе
они бы наверняка получили адреса наших (пользовательских) команд. Можете так
и сделать, но я у себя оставляю как есть.
Запустите приложение
- теперь все работает как надо!!!Мы спроектировали более-менее сносный текстовый редактор, для этого спукались
на относительно низкий (подробный) уровень программирования. Конечно, в получившемся
продукте еще много недоработок, да и такую задачу можно было бы решить в два
счета на C++Builder. Но мы-то не блокнот проектировали, а знакомились с новым
механизмом команд WPF на примере блокнота.
Вначале мы попытались проектировать без привлечения команд. Создали основу
блокнота, но управление доступностью источников реализовали
только для одной задачи Save, и то с большим трудом. Приходилось пускаться
на всякие ухищрения. Затем часть задач переключили на команды. И сразу удалось
гораздо легче и понятнее реализовать доступность всех источников пользовательского
интерфейса. Выгода от применения механизма команд очевидна.
Чтобы более-менее завершить наше блокнотоподобное приложение, немного подукрасим
его. Добавим в строку состояния, которая у нас имеет имя statusBar,
вывод информации о положении курсора. Опять же, постараемся минимально вмешиваться
в уже созданный код. Для этого все решение разместим в отдельном файле.
Выделите узел текущего
проекта и добавьте командой Project/Add New Item новый файл Code File с именем CaretPosition.cs
Заполните файл CaretPosition.cs следующим кодомusing System;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Controls;
namespace Notepad2
{
// Часть класса главного окна
partial class Window1
{
// Создаем экземпляр и регистрируем обработчики
CaretPosition caretPosition;
private void CreateCaretPosition()
{
// Отображение в StatusBar номера строки и столбца
caretPosition = new CaretPosition();// Создаем объект
caretPosition.TxtBox = txtBox1; // Присоединяем TextBox
// Дополняем StatusBar
this.statusBar.Items.Add(
new System.Windows.Controls.Separator());
this.statusBar.Items.Add(caretPosition.StrLineCol);
// Увековечиваем себя!
this.statusBar.Items.Insert(0, new System.Windows.Controls.Separator());
this.statusBar.Items.Insert(0, "Снетков В.М.");
// Регистрируем обработчик события перемещения каретки
txtBox1.SelectionChanged +=
new RoutedEventHandler(txtBox1_SelectionChanged);
}
// Обработчик инициирует вычисление и отрисовку нового положения
void txtBox1_SelectionChanged(object sender, RoutedEventArgs e)
{
caretPosition.CaretChanged();
}
}
class CaretPosition
{
// Закрытые поля
StatusBarItem strLineCol = new StatusBarItem();
TextBox txtBox = new TextBox();
// Открытые свойства
// Для добавления в строку состояния клиента
public StatusBarItem StrLineCol // Папа '-->'
{
// Только для чтения
get { return strLineCol; }
}
// Для присоединения к TextBox клиента
public TextBox TxtBox // Мама '>--'
{
// Только для записи
set { txtBox = value; }
}
// Вычисляет номер строки
int GetLine()
{
int count = 0;
int pos = 0;
int caretPos = txtBox.SelectionStart + 1; //txtBox1.CaretIndex
while (pos < caretPos)
{
count++; // Счетчик строк
pos = txtBox.Text.IndexOf("\r\n", pos);// \n - перевод строки
if (pos != -1) // Нашли очередную пару
pos += 2; // Сдвигаемся правее найденных
else
break;// Больше нет
}
return count;
}
public void CaretChanged()
{
if (!txtBox.IsFocused)
return;
int posChar = txtBox.CaretIndex;
int line = GetLine();
int column = posChar - txtBox.GetCharacterIndexFromLineIndex(line - 1) + 1;
// Обновляем в строке состояния
strLineCol.Content = String.Format(" Ln {0} \t Col {1}", line, column);
}
}
}
В файле Window1.xaml.cs добавьте в конструктор класса Window1 код вызова функции CreateCaretPosition()public Window1()
{
InitializeComponent();
// Создание жестов
this.CreateGestures();
// Дополнительные обработчики в файле EnabledControls.cs
AdditionalHandlers();
// Отображение в StatusBar номера строки и столбца
// Функция находится в файле CaretPosition.cs
CreateCaretPosition();
}
Запустите приложение
- номера строк и столбцов отображаются. Поэкспериментируйте с функциональностью,
разберитесь с кодомОбратите внимание, как мы из вновь созданного класса CaretPosition подключились
(мама) через свойство TxtBox к существующему объект txtBox1 окна
и извлекаем нужную информацию уже в новом классе. И как созданный в новом
классе элемент строки состояния подключили (папа) обратно к окну. Получился
как бы канал связи, по которому информация из объекта txtBox1 окна поступает
в экземпляр нового класса, а затем в обработанном виде возвращается обратно
в окно для отображения в объекте statusBar.
Снимок блокнота на данный момент будет таким

Видно, что строки и столбцы теперь отображаются в строке состояния, как и
ФИО. Имейте ввиду, что мы изначально приняли тезис, что завернутые строки Word Wrap считаем
продолжением одной и той же строки и позицию каретки отображаем как в длинной
незавернутой строке. Можно переделать, но не в этом суть - как сделали так
и сделали, хотя надо было бы сделать по другому...
Все необходимые для выполнения данной работы программы можно найти в прилагаемом каталоге.
В WPF используются элементы управления двух видов:
ContentControl. Такой элемент может
содержать в себе только один ближайший вложенный дочерний элемент.ItemsControl. Такие
элементы являются элементами контейнерного типа и могут содержать любое количество
ближайших вложенных дочерних элементов.В WPF структуру приложения можно рассматривать как дерево элементов. Если
приложение формируется в XAML, то дерево создается на основе C# дерево создается с
учетом того, как строятся коллекции для контейнеров, производных от ItemsControl,
и задается содержимое элементов-потомков ContentControl.
В WPF существуют три взгляда на дерево элементов:
Различия между логическими и визуальными деревьями не всегда важны
до тех пор, пока не потребуется более тонкое управление подсистемами WPF.
Эти две концепции позволяют понять, как могут повлиять на конечный результат
те или иные изменения, вносимые в разметку или код.
Логическое дерево представляет собой модель структуры зависимостей
родитель-потомок, определяемую вложенностью содержимого крупных элементов при
формировании разметки XAML. Это то, что мы можем представить себе на этапе
проектирования иерархии имеющихся элементов. На этапе компиляции часть элементов
детализируется в более мелкие.
Визуальное дерево представляет
модель отображаемых элементов интерфейса, порожденных классом Visual. При
компиляции приложения логическое дерево расширяется до визуального, поскольку
часть укрупненных элементов разбивается на атомарные.
Введение перенаправленных событий в WPF связано с принятой моделью содержимого.
Простого события для элемента стало недостаточно, поскольку он может содержать
в себе еще несколько визуальных элементов. Тогда для полного контроля над событием
контейнерного элемента пришлось бы привязывать один и тот же обработчик еще
и к событиям каждого из дочерних элементов. Всплывающее событие решает эту
проблему, оно восходит и ищет элемент с подписавшимся на него обработчиком.
Например, если кнопка в качестве содержимого имеет несколько элементов: надпись,
рисунок или другую кнопку, то щелчок курсором мыши на дочернем элементе еще
не означает щелчок на самой кнопке. Событие щелчка возбуждается дочерним для
кнопки элементом и его нужно преобразовать в событие щелчка для самой
кнопки. В этом случае срабатывает механизм маршрутизации событий ( RoutedEvent ).
Здесь ярко проявляются различия в логических и визуальных деревьях, поскольку
события на низком уровне маршрутизируются на основе визуального
дерева.
Разработчикам приложения не всегда необходимо знать, реализуется ли обрабатываемое событие как перенаправленное. Перенаправленные события имеют особое поведение, но оно остается невидимым, если событие обрабатывается на элементе, где оно возникает. Поэтому для традиционного использования различия между старым и новым механизмами работы событий несущественны.
Маршрутизированные события поддерживают
следующие стратегии маршрутизации ( RoutingStrategy ):
Direct - обрабатывается на источнике, возбудившем событиеTunnel (тоже самое - тоннельную!) - нисходящая маршрутизация
событийBubble - восходящая маршрутизация событийПрямые события ведут себя как
обычные события в .NET Framework. У нас есть три инградиента при рассмотрении
этого рода событий: элемент, событие и прикрепленный обработчик (или
несколько обработчиков для делегата события). При регистрации обработчика все
три инградиента жестко связываются. Событие элемента возбуждается только на
нем и может быть обработано только собственными обработчиками.
Туннельные события могут быть возбуждены
любым элементом. Но они всегда начинаются на корневом элементе ( Window, Page )
и передвигаются вниз по дереву элементов, пока не будут обработаны и прерваны ( остановлены )
каким-нибудь из них или не достигнут исходного элемента-источника для события.
Это позволяет находящимся выше элементам перехватить событие и обработать его
прежде, чем оно достигнет возбудившего его элемента. К именам всех туннельных
событий добавлена приставка Preview (например, PreviewMouseDown ), поэтому их
иногда еще называют событиями предварительного просмотра.
Пузырьковые события будут всплывать
вверх (распространяться) по визуальному дереву от исходного элемента, пока
не будут обработаны или не достигнут корневого элемента. Эти события распространяются
в противоположном к туннельным направлении, что позволяет обработать их
любому родительскому элементу, стоящему в логическом дереве выше исходного.
Например, обработчик события MouseDown можно прикрепить к охватывающему элементу
сетки, а не к самому элементу внутри сетки, или к корневому окну. Пузырьковые
события имеют имена, просто указывающие их действие (например, MouseDown ).
Перенаправленные события можно еще называть векторными, в отличие от обычных скалярных. Перенаправленные события могут использоваться для связи элементов дерева, так как данные событий можно менять и они сохраняются для каждого элемента в маршруте. Один элемент может изменить что-либо в данных события и это изменение будет доступно для следующего элемента в маршруте. Поэтому передача вверх перенаправляемых событий обычно используется для получения отчета об изменении входных данных или о состоянии различных элементов управления пользовательского интерфейса.
Прослушиватели перенаправленных событий и источники перенаправленных событий
не требуют для совместного использования общего события в их иерархии. Любой UIElement или ContentElement может являться прослушивателем перенаправленных
событий. Поэтому перенаправленные события можно рассматривать как
"концептуальный интерфейс", посредством которого разнородные элементы в приложении
могут обмениваться данными. Эта концепция для перенаправленных
событий особенно применима для событий ввода.
Существует разновидность пузырьковых событий, которые называются вложенными.
Большинство контейнерных элементов (например, Grid ) не имеет события Click,
потому что оно для них неконкретизировано. Однако к ним все равно можно прикрепить
(присоединить) обработчик наподобие свойства зависимости, позаимствовав событие
у дочернего элемента, который способен это событие возбудить. Это не значит,
что элемент с несвойственным ему вложенным событием будет способен его возбуждать,
но возбужденное другими и движущееся через него событие он будет способен
перехватить и обработать.
Обработчик события рекомендуется прикреплять в разметке XAML, поскольку такой
способ поддерживает и обычные и маршрутизируемые события. Синтаксис прикрепления
аналогичен определению свойств. В дескрипторе элемента мы пишем имя события
и через знак присваивания - имя обработчика, а оболочка в ответ сама сгенерирует
заготовку метода с соответствующей событию сигнатурой. Синтаксис вложенных
событий аналогичен, но имеет префикс элемента зависимости, например
<Grid ButtonBase.Click="Grid_Click">
<Button Click="Button_Click">
Генератор Click
</Button>
</Grid>
В такой конструкции при щелчке на кнопке Button возбудится событие Click.
Это событие начнет всплывать к корню визуального дерева. Прежде всего оно будет
обработано самой кнопкой, потому что мы прикрепили обработчик Button_Click.
Затем оно будет обработано обработчиком Grid_Click, поскольку мы его тоже предусмотрели.
И так далее, пока не достигнет корня визуального дерева. Даже если у кнопки Button не предусмотреть обработчик, то событие все равно будет ею возбуждено
при щелчке на ней и начнет всплывать по дереву элементов.
Под маршрутизацией событий подразумевается движение событий (event bubbling)
по дереву элементов в поиске своих обработчиков. Маршрутитизацию событий еще
называют перенаправлением событий. Механизм этого движения можно рассмотреть
на примере событий PreviewMouseDown (туннельное) и MouseDown (пузырьковое).
При щелчке курсором мыши на каком-нибудь элементе окна WPF (назовем его целевым
- target) вначале возбуждается туннельное событие PreviewMouseDown, которое
движется от корня дерева к целевому элементу. Когда оно достигнет корня дерева,
на смену ему возбуждается парное событие MouseDown, которое начинает движение
в противоположном направлении: от целевого элемента к корню дерева.
В любом элементе по маршруту
движения этих событий мы можем прикрепить соответствующий обработчик, перехватить
событие и обработать его (а затем, если нужно, остановить). Прикрепленному
обработчику вместе с событием передается объект аргументов (обычно вторым параметром).
Для событий PreviewMouseDown и MouseDown это будет объект e типа MouseButtonEventArgs, для события Click это будет объект e типа RoutedEventArgs, для события KeyDown - объект e типа KeyEventArgs, и так далее. Каждый из этих объектов имеет булево свойство Handled,
с помощью которого в любом из обработчиков можно прервать (остановить) дальнейшую
маршрутизацию события, присвоив значение e.Handled=true.
Если по пути к целевому элементу
мы останавливаем туннельное событие, то не будет возбуждаться и парное к
нему пузырьковое событие. Дальнейшая маршрутизация событий может автоматически
прерываться и другими событиями. Так например, при щелчке на кнопке вначале
возбуждается туннельное событие PreviewMouseDown, которое движется от корня
к кнопке. По достижении кнопки парное событие MouseDown не возбуждается, а
вместо него начинает всплывать сгенерированное кнопкой событие Click.
Более
того, если кнопка внутри себя содержит дочерние элементы и щелчок выполнен
по одному из них, то событие PreviewMouseDown доходит до этого целевого элемента.
Затем возбуждается парное пузырьковое событие MouseDown, но поднявшись до элемента
кнопки оно подавляется и заменяется всплывающим событием Click. Кнопка сама
устанавливает для события MouseDown флаг Handled=true и
возбуждает собственное событие щелчка. Это действие соответствует наиболее
естественному поведению кнопки.
Если не считать исключений, которые возбуждаются средой исполнения, то библиотечные
события WPF делятся на 4 группы:
События времени жизни всех элементов управления наследуются от класса FrameworkElement и указаны в таблице
| Событие | Описание |
|---|---|
Initialized |
Данное событие является обычным, а не перенаправленным, и возникает после
создания элемента и определения всех его полей и свойств. Такое состояние
характеризуется булевым свойством IsInitialized=true. На этом этапе еще
не применены стили и привязка данных. Соседние элементы окна могут быть
еще не полностью созданными |
Loaded |
Возникает после события Initialized, когда все окно закончило инициализацию
и дополнительно были применены стили и привязка данных к элементу. Это
последний этап, за которым следует визуализация элемента. В этот момент
он принимает состояние IsLoaded=true |
Unloaded |
Возбуждается сразу после удаления элемента из контейнера или его закрытия |
Класс FrameworkElement наследует и реализует интерфейс ISupportInitialize,
объявляющий методы BeginInit() и EndInit(). Эти методы автоматически
вызываются анализатором разметки, но если создавать элементы в процедурном
коде, то их следует вызывать вручную. После вызова метода EndInit() инициализация
элемента считается завершенной и возбуждается сигнализирующее событие Initialized.
Любой составной объект, имеющий ссылки на другие объекты, оформленные в нем
как свойства типов адресуемых объектов, начинает свое создание
с переферийных объектов. Эту нужно для того, чтобы при создании самого родителя
его свойствам можно было присвоить уже готовые адреса вспомогательных объектов.
Дерево элементов WPF тоже начинает строиться с листьев, то есть глубоко вложенные
элементы инициализируются до того, как будут инициализированы их родительские
контейнеры.
Есть еще события времени жизни окна:
| Событие | Описание |
|---|---|
Activated |
Возбуждается при получении окном фокуса ввода системы. По функциональности эквивалентно событию GotFocus элемента управления |
Closed |
Возбуждается после закрытия окна, когда еще объекты дерева являются
доступными и их событие Unloaded еще не возникло |
Closing |
Возникает при получении команды на закрытие окна, которую еще ее можно
отменить присвоением в обработчике свойству Cancel=true через аргумент CancelEventArgs. Если пользователь не закрывал окно, а просто вышел из
системы, то нужно обрабатывать событие уровня приложения System.Windows.Application.SessionEnding |
ContentRendered |
Возникает сразу после первой визуализации окна |
Deactivated |
Возбуждается каждый раз, когда окно теряет фокус ввода при свертывании или переключении на другое окно |
LocationChanged |
Происходит, когда местоположение окна изменяется |
SourceInitialized |
Предусмотрено для поддержки взаимодействия с Win32 |
StateChanged |
Происходит при изменении окном свойства WindowState |
События ввода через объекты-аргументы своих обработчиков передают информацию о состоянии устройств ввода при манипулировании с ними пользователей. Анализ этой информации позволяет принять решение об адекватной реакции приложения. События ввода являются маршрутизируемыми.
При нажатии клавиши пользователем, когда элемент имеет фокус, возникает несколько
событий, которые определены в классе UIElement и в порядке их возбуждения приведены
в таблице
| Событие | Стратегия маршрутизации | Описание |
|---|---|---|
PreviewKeyDown |
Tunnel | Происходит при нажатии |
KeyDown |
Bubble | Происходит при нажатии |
PreviewTextInput |
Tunnel | Происходит для символьных клавиш, когда нажатие завершено и элемент получил символ |
TextInput |
Bubble | Происходит для символьных клавиш, когда нажатие завершено и элемент получил символ |
PreviewKeyUp |
Tunnel | Происходит при отпускании |
KeyUp |
Bubble | Происходит при отпускании |
Некоторые элементы сами обрабатывают часть этих событий. Они могут блокировать
дальнейшее продвижение некоторых из них или возбуждать дополнительные события.
Например, элемент TextBox блокирует событие TextInput, а для клавиш-стрелок
блокирует и событие KeyDown, но в то же время возбуждает свое событие TextChanged при изменении содержимого поля ввода.
Построим приложение, которое продемонстрирует обработку клавиатурных событий.
Создайте решение EventsAndCommands вместе
с новым WPF -проектом KeyEvents командой File/New/Project, для этого настройте
окно мастера так
Обратите внимание на выбор версии библиотеки .NET Framework 3.0,
при более низких версиях шаблоны для WPF станут недоступными. Стартовым новый
проект тоже становится автоматически, поскольку он пока первый и единственный
в решении. Дерево Solution Explorer полужирным шрифтом.
Заполните файл разметки Window1.xaml следующим дескрипторным кодом<Window x:Class="KeyEvents.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: События клавиатуры"
Height="300"
Width="300"
MinHeight="300"
MinWidth="300"
Background="#FFD4D0C8"
>
<DockPanel LastChildFill="True" Margin="2,2,2,0">
<DockPanel DockPanel.Dock="Top" LastChildFill="True">
<Label DockPanel.Dock="Left" Content="Введите текст:" />
<TextBox Name="textBox"
PreviewKeyDown="textBox_KeyEvent"
KeyDown="textBox_KeyEvent"
PreviewKeyUp="textBox_KeyEvent"
KeyUp="textBox_KeyEvent"
PreviewTextInput="textBox_PreviewTextInput"
/>
</DockPanel>
<StackPanel DockPanel.Dock="Bottom" Margin="0,5,0,0">
<CheckBox Name="checkIgnoreRepeat"
Content="Игнорировать автогенерацию клавиш"
IsChecked="True"
Click="Check_Click"
IsTabStop="False"
/>
<CheckBox Name="checkIgnorePreviewTextInput"
Content="Игнорировать событие PreviewTextInput"
IsChecked="True"
Margin="0,2"
Click="Check_Click"
IsTabStop="False"
/>
<CheckBox Name="checkIgnoreSymbol"
Content="Запретить в TextBox нечисловые клавиши"
Margin="15,2"
Click="Check_Click"
IsTabStop="False"
/>
<CheckBox Name="checkIgnoreOther"
Content="Запретить в TextBox некоторые клавиши"
Margin="0,2"
Click="Check_Click"
IsTabStop="False"
/>
<CheckBox Name="checkConvertNumber"
Content="Конвертировать вывод цифровых клавиш"
Margin="0,2"
Click="Check_Click"
IsTabStop="False"
/>
<Button
HorizontalAlignment="Right"
Content="Очистить"
Margin="0,5,0,2"
Padding="5,0,5,0"
Click="Button_Click"
/>
</StackPanel>
<ListBox Name="listBox" Focusable="False" />
</DockPanel>
</Window>
Пройдитесь по разметке
и командой Navigate to Event Handler контекстного меню для
записей событий создайте заготовки обработчиков в файле процедурного
кодаНекоторые события связаны с одними и теми же обработчиками, но лишних обработчиков
мы таким образом все равно не создадим. Команда Navigate to Event Handler создает новый обработчик только тогда, когда он еще не существуют, а иначе
приведет только к позиционированию на уже существующий.
Запустите проект и
убедитесь, что приведенная разметка реализует следующий интерфейс окна приложения
Без создания обработчиков для указанных в разметке событий мы бы не смогли откомпилировать приложение и получить приведенное окно. Если в разметке для какого-то события зарегистрирован обработчик, то перед запуском приложения его нужно обязательно создать, пусть даже с пустым телом. Поскольку компилятор будет настойчиво искать его определение в файле процедурного кода и выдаст ошибку в случае неуспеха. Другое дело, когда обработчик создан, но не присоединен к событию. В таком случае он считается одним из методов класса окна и компилятор протестовать не будет.
Изучите код файла
разметки, обеспечивающий требуемую компоновку элементов
управления пользовательского интерфейса нашего упражненияСледующим шагом мы должны заполнить каждый из обработчиков соответствующим кодом. Но вместо того, чтобы делать это по частям, приведем весь код сразу.
Удалите из
файла Window1.xaml.cs процедурного кода окна все что там есть и заполните
его следующим содержимым, которое полностью согласуется с разметкойusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace KeyEvents
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
textBox.Focus();
}
private void textBox_KeyEvent(object sender, KeyEventArgs e)
{
if ((bool)checkIgnoreRepeat.IsChecked e.IsRepeat)
return;// Игнорировать повторные события
// Запретить действие в TextBox некоторых клавиш
if(checkIgnoreOther.IsChecked.Value)
switch (e.Key)
{
case Key.Space: // Пробел
case Key.Left: // Стрелка влево
case Key.Right: // Стрелка вправо
case Key.Home: // В начало поля
case Key.End: // В конец поля
e.Handled = true;
break;
}
string key = e.Key.ToString();
// Конвертируем вывод цифровых клавиш основной клавиатуры
if (checkConvertNumber.IsChecked.Value)
{
KeyConverter converter = new KeyConverter();
key = converter.ConvertToString(e.Key);
}
string message = e.RoutedEvent.ToString();
message = message.Substring(message.IndexOf('.') + 1);
message = String.Format("Event: {0,-25}", message) +
"\t Key: " + key;
listBox.Items.Add(message);
listBox.ScrollIntoView(message);// Видеть последний
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// Очищаем поле и список
textBox.Clear();
listBox.Items.Clear();
// Возвращаем фокус
textBox.Focus();
}
private void Check_Click(object sender, RoutedEventArgs e)
{
// Распознаем и синхронизируем взаимосвязанные CheckBox
FrameworkElement checkBox = e.Source as FrameworkElement;
switch (checkBox.Name)
{
case "checkIgnoreSymbol":
if (checkIgnoreSymbol.IsChecked.Value)
checkIgnorePreviewTextInput.IsChecked = false;
break;
case "checkIgnorePreviewTextInput":
if (checkIgnorePreviewTextInput.IsChecked.Value)
checkIgnoreSymbol.IsChecked = false;
break;
}
// Возвращаем фокус
textBox.Focus();
}
// Для отображаемых символов текстового поля
private void textBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (checkIgnorePreviewTextInput.IsChecked.Value)
return;
// Запрещаем в TextBox нечисловые символы
short val;
// Попытка преобразовать в число без генерации исключения
bool success = Int16.TryParse(e.Text, out val);
if (!success)
{
e.Handled = true;// Останавливаем событие
}
string message = e.RoutedEvent.ToString();
message = message.Substring(message.IndexOf('.') + 1);
message = String.Format("Event: {0,-25}", message) +
"\t Text: " + e.Text;
listBox.Items.Add(message);
}
}
}
В приведенном процедурном коде перехватываются события клавиатуры и в список выводится информация о нажатых клавишах, которая передается вместе с событием в обработчики через объекты их аргументов. Целевым объектом клавиатурных событий в данном коде является элемент текстового поля. Интерфейсные переключатели обеспечивают смену режимов обработки клавиатурных событий и иллюстрируют возможности библиотечных классов.
Запустите и испытайте
работу приложения, один из рабочих моментов которого может выглядеть так
Изучите приведенный
код процедурного файла, обратите внимание на некоторые приемы программированияВ обработчиках мы извлекаем информацию о событиях как RoutedEvent. Это значит,
что события клавиатуры маршрутизируемые и их можно прослушивать и перехватывать
не только в целевом объекте, но и в других местах логического дерева элементов.
Убедимся в этом:
В файле разметки переместите
(вырежьте из... и вставьте в...) весь блок регистрации событий из элемента TextBox в самый внешний контейнер DockPanel, чтобы схематично окончательный
код стал таким<Window x:Class="KeyEvents.Window1"
........................................................
>
<DockPanel LastChildFill="True" Margin="2,2,2,0"
PreviewKeyDown="textBox_KeyEvent"
KeyDown="textBox_KeyEvent"
PreviewKeyUp="textBox_KeyEvent"
KeyUp="textBox_KeyEvent"
PreviewTextInput="textBox_PreviewTextInput"
>
<DockPanel DockPanel.Dock="Top" LastChildFill="True">
<Label DockPanel.Dock="Left" Content="Введите текст:" />
<TextBox Name="textBox"
/>
</DockPanel>
<StackPanel DockPanel.Dock="Bottom" Margin="0,5,0,0">
.....................................................
</StackPanel>
<ListBox Name="listBox" Focusable="False" />
</DockPanel>
</Window>
Запустите приложение
и убедитесь, что функциональность его осталась прежнейПоставляемый событием в его обработчик объект e класса KeyEventArgs содержит не только информацию о нажатой клавише, но и сведения
о состоянии модификаторов (расширителей), таких как Shift, Ctrl, Alt. Свойство e.KeyStates информирует о том, в каком состоянии находилась клавиша в момент
генерации события: Down, None, Toggled.
Много полезной информации имеет свойство e.KeyboardDevice,
представляющее экземпляр класса KeyboardDevice. Из его свойства e.KeyboardDevice.FocusedElement можно узнать, какой элемент в данный момент имеет фокус клавиатурного ввода.
Из свойства e.KeyboardDevice.Modifiers объекта-аргумента обработчика можно
определить состояние
private void KeyEvent(object sender, KeyEventArgs e)
{
if ((e.KeyboardDevice.Modifiers ModifierKeys.Control)
== ModifierKeys.Control)
Console.WriteLine("Нажато расширение Ctrl");
// Или аналогичный код
if((e.KeyboardDevice.Modifiers ModifierKeys.Alt) > 0)
Console.WriteLine("Нажато расширение Alt");
// Или аналогичный код
if ((e.KeyboardDevice.Modifiers ModifierKeys.Shift) != 0)
Console.WriteLine("Нажато расширение Shift");
}
Объект e.KeyboardDevice имеет ряд полезных методов для прослушивания состояния
любой интересующей нас клавиши
Методы принимают клавишу, возбудившую событие, и сообщают, что с ней происходило в момент возникновения события. Вот пример
private void KeyEvent(object sender, KeyEventArgs e)
{
if (e.KeyboardDevice.IsKeyDown(e.Key))
Console.WriteLine("Клавиша {0} нажата", e.Key);
if (e.KeyboardDevice.IsKeyUp(e.Key))
Console.WriteLine("Клавиша {0} отпущена", e.Key);
// Попутно проверяем
if(e.KeyboardDevice.IsKeyToggled(Key.NumLock))
Console.WriteLine("Клавиша NumLock включена");
}
Не только в момент возникновения клавиатурного ввода мы можем получать информацию
о состоянии клавиатуры в обработчике, но и в любой интересующий нас момент
времени в любом месте программы. Для этого достаточно воспользоваться статическим
классом System.Windows.Input. Keyboard, который отслеживает клавиатуру
компьютера. По своим возможностям он значительно мощнее класса KeyboardDevice,
который мы получаем в обработчике вместе с клавиатурным событием. Вот пример
// Где-то в программе
bool Win, Ctrl, Alt, NumLock, CapsLock, ScrollLock;
Win = (Keyboard.Modifiers ModifierKeys.Windows) > 0;
Ctrl = (Keyboard.Modifiers ModifierKeys.Control) > 0;
Alt = (Keyboard.Modifiers ModifierKeys.Alt) > 0;
NumLock = Keyboard.IsKeyToggled(Key.NumLock);
CapsLock = Keyboard.IsKeyToggled(Key.Capital);
ScrollLock = Keyboard.IsKeyToggled(Key.Scroll);
В классе любого элемента управления WPF предусмотрено достаточно событий для
программного реагирования на действия мышью со стороны пользователя. Все такие
события имеют информативные названия со вставкой Mouse, например:
PreviewMouseDown, MouseDown, PreviewMouseUp, MouseUp, PreviewMouseMove, MouseMove, PreviewMouseWheel, MouseWheel, PreviewDragEnter, DragEnter, PreviewDrop, Drop и т.д.
Большинство событий унаследовано интерфейсными элементами WPF от класса UIElement,
но часть событий добавлена самостоятельно или другими классами. Так, более
поздний в цепочке наследования класс Control добавляет события PreviewMouseDoubleClick и MouseDoubleClick. Всеразличная информация о состоянии мыши передается вместе
с событием в обработчик через объект аргумента и может быть из него извлечена.
Но также, как и в случае с клавиатурным классом Keyboard, статический класс Mouse следит за состоянием мыши в реальном масштабе времени.
Все события мыши, связанные со щелчками или перемещением, передают объект
аргументов MouseButtonEventArgs, наследующий класс MouseEventArgs. В этом
объекте содержится информации о текущих координатах курсора, кнопке мыши, которая
произвела щелчок (левая/правая/средняя), состоянии кнопки (нажата/отпущена),
какой щелчок (одинарный/двойной) и многое другое. Даже если в элементе нет
события MouseClick или MouseDoubleClick, его можно легко распознать в
обработчике события MouseDown, проанализировав свойство аргумента ( MouseButtonEventArgs
e ) как e.ClickCount == 1 (одинарный щелчок) или e.ClickCount
== 2 (двойной
щелчок).
Событий мыши очень много и каждое из них имеет свою специфику, поэтому ограничимся только таким вступительным словом и перейдем к их практическому использованию.
Добавьте к решению
командой File/Add/New Project новый проект с именем ListenerEvents и
назначьте его стартовым
Выполните команду Project/ListenerEvents Properties... и настройте выпадающий список Output type на значение Console Application,
чтобы параллельно запускались графическое и консольное окна приложения
Мы будем воздействовать мышью на графическое окно, а вывод обработчиков событий наблюдать в консольном окне. Порядок выполнения обработчиков позволит проследить маршрут движение событий по дереву элементов.
Заполните файл разметки Window1.xaml следующим кодом<Window x:Class="ListenerEvents.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Маршрутизация событий" Height="300" Width="300"
Background="Red"
ToolTip="Элемент Window - Red"
PreviewMouseDown="Window_PreviewMouseDown"
MouseDown="Window_MouseDown"
ButtonBase.Click="Window_Click"
>
<Window.ContextMenu>
<ContextMenu>
<MenuItem Header="Item1" />
</ContextMenu>
</Window.ContextMenu>
<DockPanel>
<Menu
DockPanel.Dock="Top"
ToolTip="Элемент Menu - #FFD4D0C8"
Background="#FFD4D0C8"
>
<MenuItem Header="File">
<MenuItem Header="_Open" />
<MenuItem Header="_Save" />
<MenuItem Header="Save_As" />
<MenuItem Header="E_xit" />
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Header="Cu_t" />
<MenuItem Header="_Copy" />
<MenuItem Header="_Paste" />
</MenuItem>
</Menu>
<Grid
Width="220"
Height="200"
Background="Green"
ToolTip="Элемент Grid - Green"
PreviewMouseDown="Grid_PreviewMouseDown"
MouseDown="Grid_MouseDown"
ButtonBase.Click="Grid_Click"
>
<UniformGrid
Rows="3"
Height="140" Width="130"
Background="Blue"
ToolTip="Элемент UniformGrid - Blue"
PreviewMouseDown="UniformGrid_PreviewMouseDown"
MouseDown="UniformGrid_MouseDown"
ButtonBase.Click="UniformGrid_Click"
>
<TextBlock
Background="Yellow"
VerticalAlignment="Center"
TextAlignment="Center"
ToolTip="Элемент TextBlock - Yellow"
PreviewMouseDown="TextBlock_PreviewMouseDown"
MouseDown="TextBlock_MouseDown"
ButtonBase.Click="TextBlock_Click"
>
Туннельное
<LineBreak />
Пузырьковое
</TextBlock>
<TextBlock
Background="Aqua"
VerticalAlignment="Center"
TextAlignment="Center"
ToolTip="Элемент TextBlock - Aqua"
MouseEnter="TextBlock_MouseEnter"
MouseLeave="TextBlock_MouseLeave"
PreviewMouseDown="TextBlock_PreviewMouseDown"
MouseDown="TextBlock_MouseDown"
ButtonBase.Click="TextBlock_Click"
>
Прямое MouseEnter
<LineBreak />
Прямое MouseLeave
</TextBlock>
<Button
Background="Orange"
VerticalAlignment="Center"
ToolTip="Элемент Button - Orange"
PreviewMouseDown="Button_PreviewMouseDown"
MouseDown="Button_MouseDown"
Click="Button_Click"
>
Генератор Click
</Button>
</UniformGrid>
</Grid>
</DockPanel>
</Window>
Обратите внимание, что все элементы, пока, мы сделали неименованными, но среда
выполнения в точности определит, какой элемент возбудил или обработал событие.
В названиях пунктов меню мы применили знаки подчеркивания для использования
горячих клавиш, которые проявятся после нажатия клавиши Alt в работающем приложении.
В Windows Forms для этой цели используется символ амперсанда , но в XAML он конфликтовал бы с подобным управляющим символом , поэтому был заменен на
подчеркивание.
Заполните файл поддержки
разметки Window1.xaml.cs следующим кодомusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace ListenerEvents
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
int count;
private void Window_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
count = 0;
Console.Clear();
if (e.ChangedButton == MouseButton.Left)
Console.WriteLine("{0}) Window: Наблюдаю туннельное событие PreviewMouseDown", ++count);
else
e.Handled = true;
}
private void Window_MouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) Window: Наблюдаю пузырьковое событие MouseDown", ++count);
}
private void Window_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine("{0}) Window: Наблюдаю пузырьковое событие Click (как вложенное)", ++count);
}
private void Grid_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) Grid: Наблюдаю туннельное событие PreviewMouseDown", ++count);
}
private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) Grid: Наблюдаю пузырьковое событие MouseDown", ++count);
}
private void Grid_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine("{0}) Grid: Наблюдаю пузырьковое событие Click (как вложенное)", ++count);
}
private void UniformGrid_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) UniformGrid: Наблюдаю туннельное событие PreviewMouseDown", ++count);
}
private void UniformGrid_MouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) UniformGrid: Наблюдаю пузырьковое событие MouseDown", ++count);
}
private void UniformGrid_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine("{0}) UniformGrid: Наблюдаю пузырьковое событие Click (как вложенное)", ++count);
}
private void TextBlock_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) TextBlock: Наблюдаю туннельное событие PreviewMouseDown", ++count);
}
private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) TextBlock: Наблюдаю пузырьковое событие MouseDown", ++count);
}
private void TextBlock_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine("{0}) TextBlock: Наблюдаю пузырьковое событие Click (как вложенное)", ++count);
}
private void TextBlock_MouseEnter(object sender, MouseEventArgs e)
{
Console.WriteLine("{0}) TextBlock: Возбуждаю прямое событие MouseEnter", ++count);
}
private void TextBlock_MouseLeave(object sender, MouseEventArgs e)
{
Console.WriteLine("{0}) TextBlock: Возбуждаю прямое событие MouseLeave", ++count);
}
private void Button_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) Button: Наблюдаю туннельное событие PreviewMouseDown", ++count);
}
private void Button_MouseDown(object sender, MouseButtonEventArgs e)
{
Console.WriteLine("{0}) Button: Наблюдаю пузырьковое событие MouseDown", ++count);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine("{0}) Button: Возбуждаю пузырьковое событие Click", ++count);
}
}
}
Обратите внимание, что в первом обработчике мы перехватываем туннельное событие щелчка мыши сразу же в корневом элементе и подавлем его для правой кнопки. В то же время это не влияет на работу контекстного меню, поскольку в этом случае среда выполнения создает свое всплывающее окно и передает ему фокус ввода.
Запустите приложение
- получим следующее графическое окно
Для элементов логического дерева, входящих в разметку, мы определили разный
цвет фона, чтобы можно было их визуально различать. Цвет фона меню Background="#FFD4D0C8" мы
назначили в стиле HTML для напоминания, хотя
по умолчанию оно и так имеет такой фоновый системный цвет, типичный
для большинства пользовательских элементов управления (первый байт FF определяет
коэффициент непрозрачности Opacity, FF - полная непрозрачность, 0 - полная
прозрачность).
Теперь осталось последовательно щелкать на отдельные элементы графического окна и в консольном окне наблюдать за маршрутизацией событий по последовательности срабатывания обработчиков. Для удобства можно пользоваться всплывающей подсказкой. Ниже приведены результаты, для группировки которых использован цвет фона элемента:
| Цвет (элемент) | Вывод обработчиков |
|---|---|
| Red | |
| Green | |
| Blue | |
| Yellow | |
| Aqua | |
| Orange |
Обратите внимание, что хотя мы и вложили в элементы TextBlock событие Click,
но оно не проходит через них при щелчке на кнопке, поскольку сразу всплывает
к родительскому элементу UniformGrid. И любой из элементов с вложенным событием
Click, как и вообще с любым вложенным событием, способен его только слушать
и обрабатывать, но никоим образом не может его возбуждать.
Может возникнуть вопрос, а как на самом деле в коде происходит регистрация
обработчиков событий, если мы их прикрепили в разметке, ...и с помощью каких
делегатов? Все это делает за нас оболочка благодаря тому, что класс поддержки
разметки объявлен как partial (частичный). Чтобы увидеть это, надо зайти
в конструктор кодовой части файла Window1.axml.cs для класса Window1,
щелкнуть правой кнопкой мыши на вызове метода InitializeComponent() и
выбрать команду Go To Definition (Перейти к определению) из контекстного
меню. В редакторе отобразится созданный файл кода Window1.g.i.cs, где и будут
полные определения прикрепленных к элементам обработчиков событий.
В нашем примере мы применили разные типы элементов для перехвата события.
Когда применяются одинаковые типы элементов, то для правильной адресации
следует присоединять
к одному и тому же событию обработчики с уникальными именами. Но можно ко всем
элементам присоединить и общий обработчик, тогда нужно присваивать
уникальные имена уже элементам и анализировать в обработчике источник, возбудивший
событие с присоединенным обработчиком. Сказанное можно проиллюстрировать примером
из MSDN для трех именованных кнопок:
private void CommonClickHandler(object sender, RoutedEventArgs e)
{
FrameworkElement feSource = e.Source as FrameworkElement;
switch (feSource.Name)
{
case "YesButton":
// do something here ...
break;
case "NoButton":
// do something ...
break;
case "CancelButton":
// do something ...
break;
}
e.Handled=true;
}
Это распространенный прием для распознавания и обыкновенных событий C#.
Наподобие библиотечных маршрутизированных событий мы можем создавать свои собственные перенаправленные события, которые называют пользовательскими. Порядок добавления в класс событий таков:
RoutedEvent,
которое будет являться базовым при упаковке события. В соответствии с соглашением,
имя поля правильно заканчивать постфиксом Event. Имя базового поля
ассоциируется с идентификатором поля как ссылкой на объект RoutedEvent, а
не с именем создаваемого событияRegisterRoutedEvent() класса System.Windows. EventManager событие нужно зарегистрировать в среде исполнения CLR (Common Language Runtime)
и сохранить ссылку на объект события в статическом поле. В дальнейшем этот
объект можно передавать обработчикам события. Вызов метода регистрации можно
разместить в статическом конструкторе класса
или вызвать сразу при инициализации поляRoutedEventHandler, использовав
расширенный способ объявления события, который используется для упаковки
поля события. Делегат обеспечивает стандартную сигнатуру для обработчиковOnОбъявление класса регистрации событий выглядит так
public static RoutedEvent RegisterRoutedEvent(
string name,
RoutingStrategy routingStrategy,
Type handlerType,
Type ownerType
)
name - имя маршрутизируемого события. Имя должно быть уникальным для данного
типа владельца и не может быть пустой строкой или иметь значение ссылки
nullroutingStrategy - стратегия маршрутизации события, заданная в качестве
значения перечисления System.Windows.RoutingStrategy. Это перечисление содержит
элементы Tunnel, Bubble и DirecthandlerType - тип обработчика событий, который должен быть типом делегата
и не может иметь значение ссылки nullownerType - тип класса владельца маршрутизируемого события, который не
может иметь значение ссылки nullМетод регистрации возвращает объект вновь зарегистрированного маршрутизируемого
события, который нужно сохранить в статическом поле и в далнейшем упаковать
в само событие. Упаковка события нужно для присоединения обработчиков, когда
обработчики назначаются в разметке. Для прикрепления обработчиков в процедурном
коде нужно использовать метод UIElement.AddHandler() явно, как будет показано
ниже.
В данном упражнении создадим в классе собственное перенаправленное событие,
которое будет исполнять назначенную стратегию маршрутизации ( Bubble, Tunnel, Direct ).
Добавьте к решению
командой File/Add/New Project новый проект с именем UserEvents и назначьте
его стартовым
Заполните файл Window1.xaml следующей базовой разметкой пользовательского интерфейса<Window x:Class="UserEvents.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Пользовательские Routed-события" Height="300" Width="300"
Background="Red"
ToolTip="Элемент Window - Red"
MinWidth="300"
MinHeight="300"
ResizeMode="CanResizeWithGrip"
>
<DockPanel>
<Grid
Width="220"
Height="200"
Background="Green"
ToolTip="Элемент Grid - Green"
>
<UniformGrid
Height="140" Width="130"
Background="Blue"
ToolTip="Элемент UniformGrid - Blue"
>
<Button
VerticalAlignment="Center"
ToolTip="Элемент Button - #FFD4D0C8"
Margin="5,0,5,0"
>
Возбудить событие
</Button>
</UniformGrid>
</Grid>
</DockPanel>
</Window>
Чтобы не мудрствовать лукаво, на данном этапе мы взяли пустой пользовательский интерфейс из предыдущего упражнения и немного его упростили, убрав меню и контекстное меню окна.
Запустите приложение
- получим заготовку окна с полуфункциональным интерфейсом для продолжения выполнения
упражнения
Следующим шагом мы создадим класс с событием, Button, и
упакуем его в отдельный файл с именем MyButton.cs. Модель расширения кнопки
будет удобна для возбуждения нашего события по перекрытому виртуальному методу OnClick().
Добавьте к текущему
проекту командой Project/Add New Item новый файл с именем MyButton.cs по
шаблону Custom Control (WPF), как показано на рисунке
Удалите сопутствующую
папку Themes вместе с ее содержимым, которую автоматически создала оболочка
для выбранного шаблона
Шаблон Custom Control (WPF) применяется для разработки пользовательских
компонентов 'с нуля', но мы его здесь использовали потому, что в нем наиболее
полно представлены подключенные пространства имен WPF
Заполните файл MyButton.cs следующим кодомusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace UserEvents
{
// Класс определения пользовательского события Tap,
// возбуждаемого по щелчку мыши на расширенной кнопке
public class MyButton : Button
{
// Объявляем базовое поле события
public static readonly RoutedEvent TapEvent;
// Инициализируем в статическом конструкторе базовое поле события
// (можно было инициализировать сразу при объявлении поля, без конструктора)
static MyButton()
{
TapEvent = EventManager.RegisterRoutedEvent(
"Tap", // Зарегистрированное имя
RoutingStrategy.Bubble, // Стратегия перенаправления
typeof(RoutedEventHandler), // Тип делегата обработчиков
typeof(MyButton) // Тип владельца события
);
}
// Контейнер для создания обработчиков в XAML
public event RoutedEventHandler Tap
{
add { base.AddHandler(TapEvent, value); }
remove { base.RemoveHandler(TapEvent, value); }
}
}
}
Событие нужно чем-то возбудить, собственно для этого мы и выбрали расширение
именно кнопки. Переопределяемые события возбуждаются методом RaiseEvent(),
который наследуется от класса UIElement всеми элементами управления пользовательского
интерфейса (кроме документных для ContentElement ).
Добавьте в класс-расширение MyButton кнопки перекрытие виртуального метода OnClick(), наследуемого от
базового класса Button, со следующим кодомpublic static int count; // Счетчик перехвата события
// Перекроем метод щелчка базовой кнопки для возбуждения события
protected override void OnClick()
{
count = 0; // Сбрасываем счетчик
//Console.Clear(); // Очищаем консоль
base.RaiseEvent(new RoutedEventArgs(MyButton.TapEvent));// Возбуждаем событие
}
Необязательный поле-счетчик count мы ввели для нумерации узлов прохождения
прослушиваемого события при выдачи результатов. Он должен накапливать значение,
поэтому объявлен как статический, чтобы иметь область видимости уровня объекта-типа,
а не объекта-экземпляра.
Далее мы вложим пользовательское событие в элементы логического дерева интерфейса
и прикрепим к нему обработчики для прослушивания. Заметим, что владельцем
является сам класс-расширение кнопки, поэтому
обработчик щелчка нужно прикрепить прямо к событию.
Для наглядности
часть обработчиков Window1 (или индивидуально в каждый прослушивающий элемент)
нужно добавить пространство имен по синтаксису
xmlns:custom="clr-namespace:UserEvents"
с любым уникальным именем, например, custom.
Дополните разметку
следующим выделенным кодом<Window x:Class="UserEvents.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Пользовательские Routed-события" Height="300" Width="300"
Background="Red"
ToolTip="Элемент Window - Red"
MinWidth="300"
MinHeight="300"
ResizeMode="CanResizeWithGrip"
xmlns:custom="clr-namespace:UserEvents"
Name="nWindow1"
>
<DockPanel
Name="nDockPanel"
>
<Grid
Width="220"
Height="200"
Background="Green"
ToolTip="Элемент Grid - Green"
custom:MyButton.Tap="Grid_Tap"
>
<UniformGrid
Height="140" Width="130"
Background="Blue"
ToolTip="Элемент UniformGrid - Blue"
custom:MyButton.Tap="UniformGrid_Tap"
>
<custom:MyButton
VerticalAlignment="Center"
ToolTip="Элемент Button - #FFD4D0C8"
Margin="5,0,5,0"
Tap="MyButton_Tap"
>
Возбудить событие
</custom:MyButton>
</UniformGrid>
</Grid>
</DockPanel>
</Window>
Элементам Window1 и DockPanel мы только присвоили имена. Автоматически создать
для них заготовки обработчиков с нужной сигнатурой оболочка не сможет, поскольку
она их не видит. А для элементов с вложенными событиями - создаст.
Щелкайте на записях
присоединения обработчиков в элементах Grid, UniformGrid, MyButton кода разметки
и командой контекстного меню Navigate to Event Handler создайте
в процедурном коде соответствующие заготовки обработчиков
Создайте в процедурном
коде по любой из полученных заготовок еще два обработчика с именами nDockPanel_Tap() и nWindow1_Tap() и той же сигнатуройВ результате должны быть созданы обработчики с именами:
Добавьте в экземплярный
конструктор класса MyButton процедурный код динамического прикрепления
обработчиков к nDockPanel и nWindow1 следующим образом// Конструктор экземпляра
public Window1()
{
InitializeComponent();
// Динамический способ присоединения обработчиков
nWindow1.AddHandler(MyButton.TapEvent,
new RoutedEventHandler(this.nWindow1_Tap));
nDockPanel.AddHandler(MyButton.TapEvent,
new RoutedEventHandler(this.nDockPanel_Tap));
}
Добавьте в конец класса Window1 функцию ShowTap() вывода результатов прослушивания события Tap void ShowTap(object obj, RoutedEventArgs args)
{
if (MyButton.count == 0)
{
System.Diagnostics.Debug.WriteLine(
String.Format("\n\t Стратегия маршрутизации: {0}",
args.RoutedEvent.RoutingStrategy));
}
String typeName = obj.GetType().Name;
System.Diagnostics.Debug.WriteLine(
String.Format("{0}) {1}: Наблюдаю событие Tap",
++MyButton.count, typeName));
}
Данная функция будет выводить информацию о прохождении события по элементам
дерева в панель оболочки Output. Применение метода String. Format() объясняется
тем, что ни одна из 4-х перегрузок функции Debug. WriteLine() не способна принимать
строку форматирования со спецификаторами формата типа {0}, {1}, и т.д. (в отличие
от метода Console. WriteLine() ). При каждом новом возбуждении события в кнопке-источнике
мы будем добавлять
к выводу результатов заголовок с применяемой стратегией маршрутизации.
Если
у вас панель Output еще не включена, то включите ее командой меню оболочки View/OutputРезультаты можно выводить и на консольное окно. Тогда нужно выполнить следующее:
ShowTap() заменить метод
System.Diagnostics.Debug. WriteLine() на Console. WriteLine()Windows Application на Console Application командой Project/Properties (в списке Output type )
Вставьте в каждый из
созданных обработчиков Tap вызов функции ShowTap() по следующему образцуprivate void nWindow1_Tap(object sender, RoutedEventArgs e)
{
this.ShowTap(sender, e);
}
Здесь мы стремились сделать код как можно унифицированнее, чтобы в случае
смены целевого объекта вывода результатов, например, с панели Output на
консоль, не пришлось бы много исправлять.
Наконец-то наступил момент истины - время испытать наш код и подтвердить правильность механизма работы пользовательских событий, который должен быть точно таким же, как и библиотечных.
Запустите последовательно
приложение с разными значениям стратегии маршрутизации (перечисление RoutingStrategy )
при регистрации события в классе MyButton. В окне Output должен появится
следующий результат| Стратегия | Результат |
|---|---|
| RoutingStrategy.Bubble | Стратегия маршрутизации: Bubble |
| RoutingStrategy.Tunnel | Стратегия маршрутизации: Tunnel |
| RoutingStrategy.Direct | Стратегия маршрутизации: Direct |
В таблице наглядно показано, как осуществляется стратегия маршрутизации события
по дереву элементов: событие всплывает, нисходит или обрабатывается в месте
возбуждения и немедленно останавливается ( Bubble, Tunnel или Direct ).
Иногда может понадобиться такой обработчик
Для одного события можно создать несколько обработчиков уровня класса, по одному для каждого дочернего типа. Регистрация обработчиков должна выполняться только в коде, а именно в статическом конструкторе класса окна. Статический конструктор отличается тем, что он не должен иметь перегрузок, не принимает аргументы (конструктор по умолчанию) и вызывается сразу, как только объект-тип будет загружен в память (не дожидаясь создания хотя бы одного объекта-экземпляра).
Посмотрим это на простом примере, нам важен сам код регистрации обработчика уровня класса.
Добавьте в класс окна Window1 следующий код (теперь листинг файла Window1.xaml.cs приводится полностью)using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace UserEvents
{
public partial class Window1 : Window
{
/*
static Window1()
{
// Добавить владельца события ?????
MyButton.TapEvent.AddOwner(typeof(FrameworkElement));
}
*/
// Статический конструктор (без аргументов и перегрузок)
static Window1()
{
// Зарегистрировать обработчик уровня любого класса дерева, который
// сработает перед всеми обработчиками экземпляра этого класса
EventManager.RegisterClassHandler(
typeof(Window1),
MyButton.TapEvent,
new RoutedEventHandler(SuperMethod1));
EventManager.RegisterClassHandler(
typeof(System.Windows.Controls.Primitives.UniformGrid),
MyButton.TapEvent,
new RoutedEventHandler(SuperMethod2));
}
// Обработчик уровня класса
static void SuperMethod1(object sender, RoutedEventArgs e)
{
String typeName = sender.GetType().Name;
System.Diagnostics.Debug.WriteLine(
String.Format("{0}) {1}: Суперобработчик события Tab",
++MyButton.count, typeName));
}
// Обработчик уровня класса
static void SuperMethod2(object sender, RoutedEventArgs e)
{
String typeName = sender.GetType().Name;
System.Diagnostics.Debug.WriteLine(
String.Format("{0}) {1}: Суперобработчик события Tab",
++MyButton.count, typeName));
}
// Конструктор экземпляра
public Window1()
{
InitializeComponent();
// Динамический способ присоединения обработчиков
nWindow1.AddHandler(MyButton.TapEvent,
new RoutedEventHandler(this.nWindow1_Tap));
nDockPanel.AddHandler(MyButton.TapEvent,
new RoutedEventHandler(this.nDockPanel_Tap));
}
private void MyButton_Tap(object sender, RoutedEventArgs e)
{
this.ShowTap(sender, e);
}
private void UniformGrid_Tap(object sender, RoutedEventArgs e)
{
this.ShowTap(sender, e);
}
private void Grid_Tap(object sender, RoutedEventArgs e)
{
this.ShowTap(sender, e);
}
private void nDockPanel_Tap(object sender, RoutedEventArgs e)
{
this.ShowTap(sender, e);
}
private void nWindow1_Tap(object sender, RoutedEventArgs e)
{
this.ShowTap(sender, e);
}
void ShowTap(object obj, RoutedEventArgs args)
{
if (MyButton.count == 0)
{
System.Diagnostics.Debug.WriteLine(
String.Format("\n\t Стратегия маршрутизации: {0}",
args.RoutedEvent.RoutingStrategy));
}
String typeName = obj.GetType().Name;
System.Diagnostics.Debug.WriteLine(
String.Format("{0}) {1}: Наблюдаю событие Tap",
++MyButton.count, typeName));
}
}
}
Закомментированный код оставлен для размышлений по поводу метода AddOwner().
Как и где его применять, я так и не смог разобраться по MSDN.
Запустите приложение,
результат будет таким (для стратегии Bubble )| Стратегия маршрутизации: Bubble |
Объект RoutedEventArgs, который мы передали событию при возбуждении в
перекрытом методе OnClick() несет в себе только ту информацию, которую предусмотрели
разработчики библиотеки. Мы можем добавить свою информацию к нашему события,
если расширим класс RoutedEventArgs.
Поскольку стандартный делегат
public delegate void RoutedEventHandler(object sender, RoutedEventArgs e)
задает сигнатуру обработчиков такую, что второй аргумент ожидает ссылку на
объект RoutedEventArgs, то ссылку на более толстый объект-расширение обработчик
события примет спокойно. Но нужно иметь ввиду, для того, чтобы добраться до
членов расширения переданного в обработчик объекта, необходимо будет повысить
полномочия ссылки до нашего толстого объекта.
Добавьте в самый конец
файла MyButton.cs после всего существующего кода следующее расширение
библиотечного класса RoutedEventArgs объекта-аргумента событияnamespace UserEvents
{
// Расширяем класс аргументов
class MyEventArgs : RoutedEventArgs
{
// Соблюдаем сигнатуру родителя
public MyEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
{
}
string message;// Базовое поле свойства
public string Message // Свойство чтения/записи
{
get { return message; }
set { message = value; }
}
}
}
Здесь в расширение мы скромно добавили только одно свойство вместе с базовым полем хранения данных. Но при необходимости можно добавить любое количество членов расширения.
Модифицируйте перекрытый
метод OnClick() так, чтобы в возбуждаемое событие передавался объект-расширение
с начальным значением нового свойства// Перекроем метод щелчка базовой кнопки для возбуждения события
protected override void OnClick()
{
count = 0; // Сбрасываем счетчик
//Console.Clear(); // Очищаем консоль
//base.RaiseEvent(new RoutedEventArgs(MyButton.TapEvent));// Возбуждаем событие
// Добавляем информацию и отсылаем
MyEventArgs myEventArgs = new MyEventArgs(MyButton.TapEvent);
myEventArgs.Message = "Привет студентам от Снеткова!";
base.RaiseEvent(myEventArgs);// Возбуждаем событие
}
Модифицируйте в файле Window1.xaml.cs вспомогательную функцию ShowTap() следующим образомvoid ShowTap(object obj, RoutedEventArgs args)
{
if (MyButton.count == 0)
{
System.Diagnostics.Debug.WriteLine(
String.Format("\n\t Стратегия маршрутизации: {0}",
args.RoutedEvent.RoutingStrategy));
}
String typeName = obj.GetType().Name;
/*
System.Diagnostics.Debug.WriteLine(
String.Format("{0}) {1}: Наблюдаю событие Tap",
++MyButton.count, typeName));
*/
// Повышаем полномочия ссылки и извлекаем сообщение
MyEventArgs e = args as MyEventArgs;
string message = e.Message;
// Выводим информацию
System.Diagnostics.Debug.WriteLine(
String.Format("{0}) {1}: Наблюдаю событие Tap.\n"
+ "\tПолучил сообщение: {2}",
++MyButton.count, typeName, message));
// Изменяем информацию в объекте-аргументе
e.Message = String.Format("Привет студентам от {0} и {1}!",
typeName, args.RoutedEvent.OwnerType.Name);
}
В каждом обработчике мы намеренно меняем значение свойства Message объекта-аргумента
события на новое, чтобы подчеркнуть такую возможность. Один и тот же объект
аргумента движется вместе с событием и последовательно попадает в подписанные
на событие обработчики элементов, где и может быть изменен. Для получения доступа
к свойству Message нашего объекта-аргумента предварительно нужно повысить полномочия
ссылки базового типа RoutedEventArgs. Вспомним также, что в любом обработчике
мы можем остановить дальнейшее продвижение события, присвоив свойству объекта-аргумента
args. Handled значение true.
Запустите приложение,
должен получиться такой результатСтратегия маршрутизации: Bubble
1) MyButton: Наблюдаю событие Tap.
Получил сообщение: Привет студентам от Снеткова!
2) UniformGrid: Суперобработчик события Tab
3) UniformGrid: Наблюдаю событие Tap.
Получил сообщение: Привет студентам от MyButton и MyButton!
4) Grid: Наблюдаю событие Tap.
Получил сообщение: Привет студентам от UniformGrid и MyButton!
5) DockPanel: Наблюдаю событие Tap.
Получил сообщение: Привет студентам от Grid и MyButton!
6) Window1: Суперобработчик события Tab
7) Window1: Наблюдаю событие Tap.
Получил сообщение: Привет студентам от DockPanel и MyButton!
Старая добрая парадигма программирования для взаимодействия приложения и пользователя состоит в генерации событий и их обработки. События сигнализируют о возникновении определенных обстоятельств, требующих реакции приложения: будь то действия пользователя, возникновение критических ситуаций или определенных условий. События возбуждаются источниками и перехватываются обработчиками (если они есть), которые и выполняют закрепленные за ними действия в ответ на эти события.
События являются удобным инструментом программирования, делающим код более прозрачным и естественным. События - это низкий уровень модели программирования. Их применение в программировании пользовательского интерфейса, когда управление приложением на уровне задач может осуществляться через несколько разных инструментов, требующих одинаковой реакции, иногда становится громоздким. Может потребоваться не только создание нескольких обработчиков с дублирующим кодом, но и необходимость следить за состоянием сразу нескольких интерфейсных элементов, инициирующих задачу, когда действие выполнить невозможно.
Прямую реакцию приложения на определенные действия пользователя обычно называют задачами, а сами запросы на действия - командами. В WPF разработан новый механизм,
существенно упрощающий связывание задач, запросов и действий, который называется
командами. Команды, как и события, в WPF являются маршрутизованными (Routed
- направленный). Но здесь есть отличие: события могут туннелировать и всплывать,
а команды только всплывают (команды бывают только пузырьковые, восходящие,
всплывающие).
На самом деле всплывают не сами команды, а генерируемые ими маршрутизованные
события. Существует всего несколько специальных событий для поддержки команд.
В классе CommandBinding определены события PreviewCanExecute и CanExecute типа CanExecuteRoutedEventHandler, а также события PreviewExecuted и Executed типа ExecutedRoutedEventHandler. В интерфейсе ICommand, наследуемом классом RoutedCommand,
определено еще событие CanExecuteChanged. Эти события проходят по дереву элементов
до тех пор, пока не будет найден прослушивающий элемент с привязкой CommandBinding для конкретной команды. Вот и все события команд.
Команда включает в себя несколько ингредиентов:
WPF начинает искать привязку.Объекты команд порождаются классом RoutedCommand, который единственный в WPF реализует (и наследует, конечно) интерфейс ICommand,
объявленный следующим образом
public interface ICommand
{
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}
Реализующий класс RoutedCommand расширяет сигнатуру объявленных
в ICommand методов до второго аргумента target
public bool CanExecute(object parameter, System.Windows.IInputElement target) public void Execute(object parameter, System.Windows.IInputElement target)
где - аргумент parameter содержит сведения о команде, а аргумент target -
сведения о целевом элементе команды.
При возбуждении элемента-источника, к которому прикреплена команда (объект RoutedCommand ), вызывается
метод CanExecute(). Этот метод возбуждает туннельное событие PreviewCanExecute, которое начинает спускаться от корня визуального дерева
элементов к источнику команды и собирает информацию с целевых элементов о возможности
выполнения команды. Получив результаты опроса от события PreviewCanExecute метод CanExecute() возбуждает парное восходящее событие CanExecute с заданием
оповестить целевые элементы принять состояние в зависимости от полученной информации
от события PreviewCanExecute, а сам возвращает флаг возможности или невозможности
выполнения команды.
Если метод CanExecute() вернул значение true, значит исполнение команды
возможно и следом вызывается метод Execute(), который последовательно возбуждает
парные события PreviewExecuted и Executed как указание выполнить команду во
всех целевых элементах прикрепленными к ним обработчиками. Событие отмены выполнения CanExecuteChanged вызывается, если диспетчер команд, в котором централизованно
выполняются командные операции, обнаруживает в источнике команды изменения,
которые могут сделать недействительной команду, вызванную, но еще не выполненную
в целевом элементе.
Библиотека .NET Framework имеет множество готовых команд, наиболее часто встречающихся
в приложениях. Команды представлены в пространствах имен System.Windows. Input и System.Windows. Documents следующими статическими классами:
В каждом из приведенных классов команда объявлена как статическое
свойство только для чтения типа RoutedUICommand (или RoutedCommand ), следовательно
при использовании команды создавать экземпляр класса команды не нужно. Команды
имеют имена, ассоциируемые с выполняемой задачей, например, Cut, Copy, New и т.д.
Чтобы команда работала, ее нужно присоединить к свойству Command источника
команды. Источников, к которым присоединена одна и та же команда, может быть
несколько. Элементы, к которым можно присоединить команду, должны наследовать
интерфейс ICommandSource. Только такие элементы будут иметь свойство Command.
Этих элементов в WPF немного и все они показаны на рисунке

Интерфейс ICommandSource реализует свои свойства в ближайших наследниках ButtonBase, Hyperlink, InputBinding, MenuItem по общему синтаксису:
public System.Windows.Input.ICommand Command { get; set; }
public object CommandParameter { get; set; }
public System.Windows.IInputElement CommandTarget { get; set; }
В источнике для присоединения команды достаточно присвоить значение только
ссылке Command на объект команды (на экземпляр RoutedCommand или RoutedUICommand ). Остальные свойства являются необязательными и служат для
уточнения команды:
CommandParameter - тип данных, определяемый пользователем, который используется
для передачи информации обработчикам, реализующим командуCommandTarget - идентифицирует элемент, в котором должна выполняться данная
команда. Если значение свойства CommandTarget не задано, в качестве цели
команды используется элемент, в котором установлен фокус вводаОбратите внимание, что два наследуемые источниками команд свойства-ссылки Command и CommandTarget имеют типы интерфейсов, что позволяет адресовать любые
команды и целевые элементы, наследующие эти интерфейсы.
Вот, например, как можно присоединить команду Paste к элементу меню в разметке
<StackPanel>
<Menu Header="Edit">
<MenuItem Command="ApplicationCommands.Paste" />
</Menu>
<TextBox />
</StackPanel>
Для библиотечных команд синтаксис разметки допускает использование сокращенной записи, например
<StackPanel>
<Menu Header="Edit">
<MenuItem Command="Paste" />
</Menu>
<TextBox />
</StackPanel>
Теперь, если объект TextBox имеет фокус ввода и буфер обмена содержит текст,
то при выборе пользователем этого пункта меню текст из буфера будет вставлен
в элемент TextBox. Обратите
внимание, что объект MenuItem не устанавливает свойство Header для отображения
в нем названия команды Paste. Если свойство для текста названия команды в источнике
опущено, то источник способен сам извлекать эту информацию из объекта присоединенной
команды.
А вот альтернативный вариант присоединения команды Paste к элементу меню
в процедурном коде
// Создание объектов пользовательского интерфейса StackPanel mainStackPanel = new StackPanel(); TextBox pasteTextBox = new TextBox(); Menu stackPanelMenu = new Menu(); MenuItem pasteMenuItem = new MenuItem(); // Добавление объектов в коллекции панели и меню stackPanelMenu.Items.Add(pasteMenuItem); mainStackPanel.Children.Add(stackPanelMenu); mainStackPanel.Children.Add(pasteTextBox); // Присоединение библиотечной команды Paste к источнику команд pasteMenuItem.Command = ApplicationCommands.Paste;
Аналогичный пример можно привести для присоединения команды к контекстному меню.
<StackPanel>
<StackPanel.ContextMenu>
<ContextMenu>
<MenuItem Command="ApplicationCommands.Paste" />
</ContextMenu>
</StackPanel.ContextMenu>
</StackPanel>
// Создание объектов пользовательского интерфейса StackPanel cmdSourcePanel = new StackPanel(); ContextMenu cmdSourceContextMenu = new ContextMenu(); MenuItem cmdSourceMenuItem = new MenuItem(); // Добавление ContextMenu в StackPanel cmdSourcePanel.ContextMenu = cmdSourceContextMenu; cmdSourcePanel.ContextMenu.Items.Add(cmdSourceMenuItem); // Присоединение библиотечной команды Paste к источнику команд cmdSourceMenuItem.Command = ApplicationCommands.Paste;
Как только мы присоединим команду к элементу-источнику, он сразу же становится
недоступен, как будто бы его свойство IsEnabled приобрело значение false. Это
происходит потому, что мы не привязали команду к прослушивающему ее элементу.
Как же это сделать?
Любой элемент управления WPF наследует от класса System.Windows. UIElement,
который имеет свойство-коллекцию CommandBindings типа CommandBindingCollection,
предназначенную для того, чтобы можно было сделать этот элемент прослушивающим
команды. В эту коллекцию помещаются специальные объекты привязки CommandBinding,
у которых имеется свойство Command и событие Executed. Мы создаем объекты CommandBinding,
присоединяем к их свойству Command нужные объекты команд, а к событию Executed - соответствующие обработчики. Затем эти настроенные объекты добавляем
в коллекцию CommandBindings элемента. Так элемент становится прослушивающим.
Как уже говорилось, когда источник команды возбуждает присоединенный к нему
объект команды, тот последовательно выполняет методы CanExecute() и Execute() интерфейса ICommand. Эти методы генерируют командные события, которые
движутся по дереву элементов, проверяя их коллекции CommandBindings с целью
обнаружить объект CommandBinding, соответствующий возбужденной команде. Как
только такой объект привязки будет обнаружен в прослушивающем команду элементе,
немедленно выполнится подписанный на событие Executed обработчик.
В качестве прослушивающего можно сделать любой элемент
логического дерева. Но для получения наибольшей гибкости привязки команд рекомендуется
добавлять в корневой элемент - окно (или контейнер элемента Page ).
Построим простое приложение, в котором присоединим библиотечные команды ApplicationCommands. Open и ApplicationCommands. Save, каждую - к двум элементам-источникам: кнопке и
главному меню окна. Дополнительным источником команд будет уже встроенная
в них клавиатурная комбинация Ctrl+O и Ctrl+S. В качестве прослушивающего элемента
выберем окно. В коллекцию CommandBindings окна добавим два объекта привязки CommandBinding, связывающие между собой команду и обработчик. Привязку выполним
в разметке XAML.
Добавьте к решению
командой оболочки File/Add/New Project новый проект с именем BindingCommandsXAML и
назначьте его стартовым в панели Solution Explorer командой контекстного
меню Set as StartUp Project
Заполните файл разметки Window1.xaml следующим кодом<Window x:Class="BindingCommandsXAML.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Декларативная привязка" Height="300" Width="300"
Background="#FFD4D0C8"
WindowStartupLocation="CenterScreen"
>
<StackPanel Margin="5">
<Menu>
<MenuItem Header="_File">
<MenuItem Command="ApplicationCommands.Open" />
<MenuItem Command="ApplicationCommands.Save" />
</MenuItem>
</Menu>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Open"
Content="Open"
/>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Save"
Content="Save"
/>
</StackPanel>
</Window>
Мы присоединили к интерфейсным элементам объекты команд и сделали их таким образом источниками этих команд.
Запустите проект и
убедитесь, что источники команд не получают фокус ввода и являются недоступнымиНесмотря на то, что в элементах меню нами не были заданы атрибуты Header,
команды сами прописали нужные названия команд и добавили подсказки для горячих
клавиш.
Добавьте в разметку
объекты привязки следующим образом<Window x:Class="BindingCommandsXAML.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Декларативная привязка" Height="300" Width="300"
Background="#FFD4D0C8"
WindowStartupLocation="CenterScreen"
>
<Window.CommandBindings>
<CommandBinding
Command="ApplicationCommands.Open"
Executed="OpenCommand_Executed">
</CommandBinding>
<CommandBinding
Command="ApplicationCommands.Save"
Executed="SaveCommand_Executed">
</CommandBinding>
</Window.CommandBindings>
<StackPanel Margin="5">
<Menu>
<MenuItem Header="_File">
<MenuItem Command="ApplicationCommands.Open" />
<MenuItem Command="ApplicationCommands.Save" />
</MenuItem>
</Menu>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Open"
Content="Open"
/>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Save"
Content="Save"
/>
</StackPanel>
</Window>
Вызовите контекстное
меню для записей привязки обработчиков и командой Navigate to Event Handler создайте заготовки обработчиков в файле процедурного кода
Запустите приложение
и убедитесь, что источники команд стали доступными, хотя обработчики еще пустые
В меню оболочки командой Project/Add Reference добавьте к текущему проекту ссылку на библиотечную
сборку System.Windows.Forms.dllМы уже обсуждали ранее, что нельзя напрямую размещать в окне WPF интерфейсные
элементы Windows Forms, для этого предназначен переходной контейнер <WindowsFormsHost>.
Но в нашем случае мы будем открывать самостоятельные диалоговые окна Windows Forms и не собираемся смешивать в одном окне интерфейсные элементы из разных технологий.
Дополните в процедурном
файле Window1.xaml.cs класс Window1 следующим кодомpublic partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
// Обработчик команды Open
System.Windows.Forms.OpenFileDialog openFileDialog = null;
private void OpenCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
if (openFileDialog == null)
openFileDialog = new System.Windows.Forms.OpenFileDialog();
openFileDialog.ShowDialog();
this.Focus();
}
// Обработчик команды Save
System.Windows.Forms.SaveFileDialog saveFileDialog = null;
private void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
if (saveFileDialog == null)
saveFileDialog = new System.Windows.Forms.SaveFileDialog();
saveFileDialog.ShowDialog();
this.Focus();
}
}
Запустите приложение
и испытайте возможность запуска каждой команды через любой из трех источников:
кнопку, меню или клавиатурную комбинациюРекомендуется при совместном использовании библиотек WPF и Windows Forms не подключать пространство имен System.Windows.Forms в
инструкции using, а использовать полное имя требуемых классов Windows Forms,
что мы и сделали. Это нужно для того, чтобы избежать конфликта имен, поскольку
в библиотеках разных технологий встречаются типы с одинаковыми именами, например, Color, Pen, Brush.
Для тренировки, перекройте
в классе Window1 функцию диспетчеризации OnKeyDown() клавиатурного события KeyDown. Код начните вводить с ключевого слова override, чтобы воспользоваться
услугами подсказчика кода IntelliSense
Заполните созданную
оболочкой заготовку кодом, обеспечивающим закрытие окна приложения по нажатию
клавиши Esc// Выход по клавише Esc с предупреждением
protected override void OnKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Escape:
MessageBoxResult result =
MessageBox.Show("Закрыть приложение ?", "",
MessageBoxButton.OKCancel,
MessageBoxImage.Question);
if (result == MessageBoxResult.OK)
this.Close();
break;
}
}
Запустите приложение,
раскройте мышью меню File и нажимайте на клавишу EscМы видим, что клавиша Esc действует только на те элементы, которым на момент нажатия передан
фокус ввода. Но если бы мы перекрыли функцию OnPreviewKeyDown(), то окно
приложения закрывалось бы сразу. Для проверки этого можно в коде просто поменять
имя перекрытой функции с OnKeyDown на OnPreviewKeyDown, так как обе функции
имеют одинаковую сигнатуру.
Для сравнения можно привести код использования равнозначного по функциональности
класса диалогового окна из библиотеки System.Windows.Forms.dll
protected override void OnKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Escape:
System.Windows.Forms.DialogResult result =
System.Windows.Forms.MessageBox.Show("Закрыть приложение ?", "",
System.Windows.Forms.MessageBoxButtons.OKCancel,
System.Windows.Forms.MessageBoxIcon.Question);
if (result == System.Windows.Forms.DialogResult.OK)
this.Close();
break;
}
}
Коды немного различаются, причем последний работает похуже - после его закрытия
окно WPF теряет фокус ввода. Но мы немного отвлеклись от темы команд WPF, продолжим...
Названия кнопок в разметке мы жестко подписали, но существует другой способ,
когда подпись можно извлечь из самой команды (если она есть). Если объект команды
произведен от класса RoutedCommand, то там нет свойства Text, которое можно
было бы использовать. Но если команда имеет тип RoutedUICommand, то этот класс
добавляет такое свойство и оно может иметь значение.
Перепишите элементы
кнопок в разметке файла Window1.xaml следующим образом<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Open"
Content="{x:Static ApplicationCommands.Open}"
/>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Save"
Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"
/>
Запустите приложение
- сейчас надписи на кнопках должны выглядеть так
Команды WPF можно вызывать и напрямую, а необязательно присоединять к источникам.
Правда, это будет уже извращение и сам механизм команд будет использоваться
только частично. При таком подходе придется реагировать на состояния источников
и на возможность выполнения команды самостоятельным кодом. Но знать о существовании
такой возможности программисту следует. Покажем это...
Добавьте к интерфейсу
окна еще четыре кнопки так<Window x:Class="BindingCommandsXAML.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Декларативная привязка" Height="300" Width="300"
Background="#FFD4D0C8"
WindowStartupLocation="CenterScreen"
>
<Window.CommandBindings>
<CommandBinding
Command="ApplicationCommands.Open"
Executed="OpenCommand_Executed">
</CommandBinding>
<CommandBinding
Command="ApplicationCommands.Save"
Executed="SaveCommand_Executed">
</CommandBinding>
</Window.CommandBindings>
<StackPanel Margin="5">
<Menu>
<MenuItem Header="_File">
<MenuItem Command="ApplicationCommands.Open" />
<MenuItem Command="ApplicationCommands.Save" />
</MenuItem>
</Menu>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Open"
Content="{x:Static ApplicationCommands.Open}"
/>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Save"
Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"
/>
<Button Margin="5" Padding="5" Focusable="False"
Content="DirectCommandsOpen"
Click="directCommandsOpen_Click"
/>
<Button Margin="5" Padding="5" Focusable="False"
Content="DirectCommandsSave"
Click="directCommandsSave_Click"
/>
<Button Margin="5" Padding="5" Focusable="False"
Content="DirectBindingsOpen"
Click="directBindingsOpen_Click"
/>
<Button Margin="5" Padding="5" Focusable="False"
Content="DirectBindingsSave"
Click="directBindingsSave_Click"
/>
</StackPanel>
</Window>
Вызовите контекстное
меню для записей события Click и командой Navigate to Event Handler создайте четыре обработчика в файле процедурного кода
Заполните обработчики
следующим кодом прямого вызова командprivate void directCommandsOpen_Click(object sender, RoutedEventArgs e)
{
ApplicationCommands.Open.Execute(null, this);
}
private void directCommandsSave_Click(object sender, RoutedEventArgs e)
{
ApplicationCommands.Save.Execute(null, this);
}
private void directBindingsOpen_Click(object sender, RoutedEventArgs e)
{
this.CommandBindings[0].Command.Execute(null);
}
private void directBindingsSave_Click(object sender, RoutedEventArgs e)
{
this.CommandBindings[1].Command.Execute(null);
}
В прямых вызовах двух первых обработчиков начальный параметр метода Execute() ожидает объект с дополнительной информацией, который будет передан на обработчик
команды. Второй параметр принимает целевой элемент привязки команды с возможностями
интерфейса IInputElement, который будет прослушивать команду и вызывать обработчик.
В нашем случае это объект окна. Два следующих обработчика используют вызовы
команд из коллекции прослушивающего элемента (окна), привязанные к нему в разметке.
Запустите приложение
и убедитесь, что добавленные кнопки инициируют прямой вызов командЧастично повторим предыдущее упражнение, но привязку команд выполним в процедурном коде.
Добавьте к решению
командой File/Add/New Project новый проект с именем BindingCommandsCode и назначьте его стартовым
Откорректируйте разметку
окна Window1 так<Window x:Class="BindingCommandsCode.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Кодовая привязка" Height="300" Width="300"
Background="#FFD4D0C8"
WindowStartupLocation="CenterScreen"
>
<StackPanel Margin="5">
<Menu>
<MenuItem Header="_File">
<MenuItem Command="ApplicationCommands.Open" />
<MenuItem Command="ApplicationCommands.Save" />
</MenuItem>
</Menu>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Open"
Content="Open"
/>
<Button Margin="5" Padding="5" Focusable="False"
Command="ApplicationCommands.Save"
Content="Save"
/>
</StackPanel>
</Window>
Заметьте, что в этой разметке отсутствует как секция привязки команд, так и ссылки на обработчики команд. Все это мы выполним в процедурной части окна.
Внесите следующие изменения
в класс Window1 файла процедурного кода Window1.xaml.cspublic partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
// Регистрация обработчика
this.Loaded += new RoutedEventHandler(Window1_Loaded);
}
void Window1_Loaded(object sender, RoutedEventArgs e)
{
// Привязка команд в коде
CommandBinding binding = new CommandBinding();
binding.Command = ApplicationCommands.Open;
binding.Executed +=
new ExecutedRoutedEventHandler(OpenCommand_Executed);
this.CommandBindings.Add(binding);
binding = new CommandBinding();
binding.Command = ApplicationCommands.Save;
binding.Executed +=
new ExecutedRoutedEventHandler(SaveCommand_Executed);
this.CommandBindings.Add(binding);
}
void OpenCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("Выполнена команда Open");
}
void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("Выполнена команда Save");
}
}
Запустите приложение
и убедитесь в работоспособности механизма команд WPF
Комбинации клавиш, предоставляющие прямой InputGestures, а можно хранить
в прослушивающем элементе в коллекции InputBindings наряду с командой. Кроме
интерфейсных элементов жесты могут служить еще одним источником команд. Команда,
имеющая жесты в своей коллекции, может вообще не быть присоединенной ни к одному
из визуальных элементов, а возбуждаться только жестами.
Жесты представленны абстрактным классом System.Windows.Input. InputGesture.
Этот класс наследует двум типам жестов: клавиатурных и мыши, которые представлены
объектами KeyGesture и MouseGesture соответственно. Команды, в свою очередь,
имеют коллекцию жестов InputGestures, которую заранее можно наполнить объектами
нужных жестов клавиатуры и мыши. Комбинации клавиш и кнопок мыши проще
всего добавлять в объект жеста через его конструктор при создании экземпляра
по схеме ( Key, ModifierKeys ) и ( MouseAction, ModifierKeys ).
В следующем примере показано, как в команду можно добавить жесты, которые будут ее запускать
// Клавиатурный жест Control+F
InputGesture gesture = new KeyGesture(Key.F, ModifierKeys.Control, "Ctrl+F");
ApplicationCommands.Find.InputGestures.Add(gesture);
// Комбинированный жест Control+LeftClick
gesture = new MouseGesture(MouseAction.LeftClick, ModifierKeys.Control);
ApplicationCommands.Find.InputGestures.Add(gesture);
// Клавиатурный жест Control+Q
KeyGesture keyGesture = new KeyGesture(Key.Q, ModifierKeys.Control, "Ctrl+Q");
ApplicationCommands.Find.InputGestures.Add(keyGesture);
// Комбинированный жест Alt+LeftClick
MouseGesture mouseGesture = new MouseGesture();
mouseGesture.MouseAction = MouseAction.LeftClick;
mouseGesture.Modifiers = ModifierKeys.Alt;
ApplicationCommands.Find.InputGestures.Add(mouseGesture);
//gesture = new MouseGesture(MouseAction.LeftClick, ModifierKeys.Alt);
//ApplicationCommands.Find.InputGestures.Add(gesture);
Теперь команда Find будет запускаться щелчком на кнопке, щелчком левой кнопки
мыши с нажатой клавишей Ctrl, комбинациями клавиш Ctrl+F или Ctrl+Q.
Продолжим модификацию нашего упражнения, чтобы проиллюстрировать сказанное о жестах.
Добавьте в обработчик
события Loaded класса окна Window1 следующий код, модифицирующий жесты библиотечных
команд Open и Savepublic partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
// Регистрация обработчика
this.Loaded += new RoutedEventHandler(Window1_Loaded);
}
void Window1_Loaded(object sender, RoutedEventArgs e)
{
// Привязка команд в коде
CommandBinding binding = new CommandBinding();
binding.Command = ApplicationCommands.Open;
binding.Executed +=
new ExecutedRoutedEventHandler(OpenCommand_Executed);
this.CommandBindings.Add(binding);
binding = new CommandBinding();
binding.Command = ApplicationCommands.Save;
binding.Executed +=
new ExecutedRoutedEventHandler(SaveCommand_Executed);
this.CommandBindings.Add(binding);
// Очистка коллекций прежних жестов команд
ApplicationCommands.Open.InputGestures.Clear();
ApplicationCommands.Save.InputGestures.Clear();
// Добавление новых жестов клавиатуры Alt+O и Alt+S
InputGesture key = new KeyGesture(Key.O, ModifierKeys.Alt, "Alt+O");
ApplicationCommands.Open.InputGestures.Add(key);
//
KeyGesture keyGesture = new KeyGesture(Key.S, ModifierKeys.Alt, "Alt+S");
ApplicationCommands.Save.InputGestures.Add(keyGesture);
// Добавление новых жестов мыши Ctrl+LeftClick и Ctrl+RightClick
InputGesture mouse = new MouseGesture(MouseAction.LeftClick, ModifierKeys.Control);
ApplicationCommands.Open.InputGestures.Add(mouse);
//
MouseGesture mouseGesture = new MouseGesture();
mouseGesture.MouseAction = MouseAction.RightClick;
mouseGesture.Modifiers = ModifierKeys.Control;
ApplicationCommands.Save.InputGestures.Add(mouseGesture);
}
void OpenCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("Выполнена команда Open");
}
void SaveCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("Выполнена команда Save");
}
}
Вначале мы очистили коллекции команд от прежних жестов, затем добавили свои жесты, в том числе с участием мыши. Коллекции можно было и не очищать, тогда работали бы прежние жесты наряду с вновь добавленными.
Запустите приложение
и убедитесь, что теперь прежние клавиатурные комбинации Ctrl+O и Ctrl+S не
работают, вместо них программа реагирует на жесты клавиатуры Alt+O и Alt+S, а также выполняются комбинированные жесты с участием мыши Ctrl+LeftClick и Ctrl+RightClickВ нашей программе после добавления жестов появился существенный недостаток
- сами жесты мы поменяли, но в меню все-таки остались старые маркеры команд Ctrl+O и Ctrl+S. Хоть мы и пытались в конструкторе KeyGesture() определить в последнем параметре displayString новые маркеры для жестов в меню,
но все осталось по прежнему. Дело здесь в том, что объект меню создается и
инициализируется в конструкторе класса по объекту уже присоединенной к источнику
команды. И это происходит раньше, чем мы в коде начинаем модифицировать саму
команду.
Чтобы это исправить, можно воспользоваться несколькими способами:
Определитете в разметке
в элементах меню свойство InputGestureText следующим образом<Menu>
<MenuItem Header="_File">
<MenuItem Command="ApplicationCommands.Open"
InputGestureText="Alt+O" />
<MenuItem Command="ApplicationCommands.Save"
InputGestureText="Alt+S" />
</MenuItem>
</Menu>
Запустите приложение
и убедитесь, что теперь маркеры команд меню изменилисьВместо разметки изменить свойство InputGestureText можно в процедурном коде, но для этого элементам меню нужно присвоить имена.
Удалите в разметке
из дескрипторов элементов меню параметры Command и InputGestureText, а добавьте
вместо них имена элементов, как показано ниже<Menu>
<MenuItem Header="_File">
<MenuItem Name="openMenu" />
<MenuItem Name="saveMenu" />
</MenuItem>
</Menu>
Добавьте в самый конец
обработчика события Loaded после кода добавления жестов следующую пару строк
динамического присоединения модифицированных команд к меню-источникуvoid Window1_Loaded(object sender, RoutedEventArgs e)
{
// Привязка команд в коде
CommandBinding binding = new CommandBinding();
binding.Command = ApplicationCommands.Open;
binding.Executed +=
new ExecutedRoutedEventHandler(OpenCommand_Executed);
this.CommandBindings.Add(binding);
binding = new CommandBinding();
binding.Command = ApplicationCommands.Save;
binding.Executed +=
new ExecutedRoutedEventHandler(SaveCommand_Executed);
this.CommandBindings.Add(binding);
// Очистка коллекций прежних жестов команд
ApplicationCommands.Open.InputGestures.Clear();
ApplicationCommands.Save.InputGestures.Clear();
// Добавление новых жестов клавиатуры Alt+O и Alt+S
InputGesture key = new KeyGesture(Key.O, ModifierKeys.Alt, "Alt+O");
ApplicationCommands.Open.InputGestures.Add(key);
//
KeyGesture keyGesture = new KeyGesture(Key.S, ModifierKeys.Alt, "Alt+S");
ApplicationCommands.Save.InputGestures.Add(keyGesture);
// Добавление новых жестов мыши Ctrl+LeftClick и Ctrl+RightClick
InputGesture mouse = new MouseGesture(MouseAction.LeftClick, ModifierKeys.Control);
ApplicationCommands.Open.InputGestures.Add(mouse);
//
MouseGesture mouseGesture = new MouseGesture();
mouseGesture.MouseAction = MouseAction.RightClick;
mouseGesture.Modifiers = ModifierKeys.Control;
ApplicationCommands.Save.InputGestures.Add(mouseGesture);
// Присоединяем модифицированные команды к меню-источнику
openMenu.Command = ApplicationCommands.Open;
saveMenu.Command = ApplicationCommands.Save;
}
Запустите приложение
и убедитесь, что теперь маркеры команд меню изменились в соответствии с введенными
нами при формировании новых жестов
Разметку меню сделайте
такой<Menu>
<MenuItem Header="_File">
<MenuItem Name="openMenu"
Command="ApplicationCommands.Open" />
<MenuItem Name="saveMenu"
Command="ApplicationCommands.Save" />
</MenuItem>
</Menu>
Замените в обработчике Window1_Loaded () только что добавленный код на новый// Присоединяем модифицированные команды к меню-источнику
//openMenu.Command = ApplicationCommands.Open;
//saveMenu.Command = ApplicationCommands.Save;
openMenu.InputGestureText = "Alt+O";
saveMenu.InputGestureText = "Alt+S";
Запустите приложение
и убедитесь, что маркеры команд меню стали правильнымиЕсть еще один способ добавления жестов, о котором стоит упомянуть. Ранее
обсуждалось, что все пользовательские элементы WPF наследуют от UIElement или ContentElement, а эти классы, в свою очередь, имеют в качестве свойства коллекцию InputBindings типа InputBindingCollection. Коллекция может быть заполнена классами KeyBinding или MouseBinding, каждый из которых связывает жест клавиатуры (представленный
объектом KeyGesture ) или мыши (представленный объектом MouseGesture ) с командой.
Оба класса наследуют тип InputBinding.
Например, привязать жесты с командой к объекту окна можно одним из следующих способов
<Window x:Class="Tmp.window"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="300">
<Window.InputBindings>
<KeyBinding Key="F"
Modifiers="Control"
Command="ApplicationCommands.Find" />
<MouseBinding MouseAction="Control+LeftClick"
Command="ApplicationCommands.Find" />
<KeyBinding Key="Q"
Modifiers="Control"
Command="ApplicationCommands.Find" />
<MouseBinding Gesture="Alt+LeftClick"
Command="ApplicationCommands.Find" />
</Window.InputBindings>
<Grid>
<Button Command="ApplicationCommands.Find"
Height="23"
Width="75"
>
Поиск
</Button>
</Grid>
</Window>
В классе MouseBinding свойство Modifiers доступно только для чтения,
поэтому жест для мыши нужно вводить в свойство MouseAction сразу целиком,
как показано в только что приведенном примере.
// Клавиатурный жест Control+F
KeyGesture keyGesture = new KeyGesture(Key.F, ModifierKeys.Control);
KeyBinding keyBinding = new KeyBinding(ApplicationCommands.Find, keyGesture);
this.InputBindings.Add(keyBinding);
// Комбинированный жест Control+LeftClick
MouseGesture mouseGesture = new MouseGesture(MouseAction.LeftClick, ModifierKeys.Control);
MouseBinding mouseBinding = new MouseBinding(ApplicationCommands.Find, mouseGesture);
this.InputBindings.Add(mouseBinding);
// Клавиатурный жест Control+Q
InputGesture gesture = new KeyGesture(Key.Q, ModifierKeys.Control);
ICommand command = ApplicationCommands.Find;
keyBinding = new KeyBinding(command, (KeyGesture)gesture);
this.InputBindings.Add(keyBinding);
// Комбинированный жест Alt+LeftClick
gesture = new MouseGesture();
((MouseGesture)gesture).MouseAction = MouseAction.LeftClick;
((MouseGesture)gesture).Modifiers = ModifierKeys.Alt;
mouseBinding = new MouseBinding();
mouseBinding.Command = command;
mouseBinding.Gesture = gesture;
this.InputBindings.Add(mouseBinding);
В данном упражнении
мы разработаем приложение, частично имитирующее текстовый блокнот, но механизм
команд WPF применять не будем. В последующем упражнении на базе разработанного
кода мы создадим альтернативный вариант текстового блокнота, но уже с
использованием команд. Ух-х-х, поехали!
Командой Add/New Project добавьте к решению новый проект WPF Application с именем Notepad1 и назначьте его стартовымОсновную часть интерфейса приложения, как и положено, мы создадим декларативно
в разметке XAML, а управление - в части процедурного кода C#.
Отредактируйте интерфейсную
часть окна следующим образом<Window x:Class="Notepad1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
>
<DockPanel>
</DockPanel>
</Window>
Добавьте в дескриптор <DockPanel> </DockPanel> разметку создания меню<DockPanel LastChildFill="True">
<!-- Меню -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_New" InputGestureText="Ctrl+N">
</MenuItem>
<MenuItem Header="_Open..." InputGestureText="Ctrl+O" />
<MenuItem Header="_Save" InputGestureText="Ctrl+S" />
<MenuItem Header="Save _As..." />
<Separator />
<MenuItem Header="Page Set_up..." />
<MenuItem Header="P_rint Preview" InputGestureText="Ctrl+F2" />
<MenuItem Header="_Print..." InputGestureText="Ctrl+P" />
<Separator />
<MenuItem Header="E_xit" />
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Header="_Undo" InputGestureText="Ctrl+Z" />
<MenuItem Header="_Redo" InputGestureText="Ctrl+Y" />
<Separator></Separator>
<MenuItem Header="Cu_t" InputGestureText="Ctrl+X" />
<MenuItem Header="_Copy" InputGestureText="Ctrl+C" />
<MenuItem Header="_Paste" InputGestureText="Ctrl+V" />
<MenuItem Header="De_lete" InputGestureText="Del" />
<Separator></Separator>
<MenuItem Header="_Find..." InputGestureText="Ctrl+F" />
<MenuItem Header="Find _Next" InputGestureText="F3" />
<MenuItem Header="_Replace..." InputGestureText="Ctrl+H" />
<MenuItem Header="_Go To..." InputGestureText="Ctrl+G" />
<Separator></Separator>
<MenuItem Header="Select _All" InputGestureText="Ctrl+A" />
</MenuItem>
<MenuItem Header="F_ormat">
<MenuItem Header="_Font..." />
<Separator />
<MenuItem Header="_Word Wrap" IsCheckable="True" IsChecked="True" InputGestureText="Ctrl+W" />
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Header="_About" />
</MenuItem>
</Menu>
</DockPanel>
Знаки подчеркивания мы использовали для того, чтобы выделить символы, по которым
пользователь может раскрывать меню и запускать команды после нажатия клавиш Alt или F10. Отмеченные символы
должны быть уникальными как на горизонтальной линейке на уровне разделов, так
и в рамках одного раздела меню. Каждому пункту присвоено название команды и
клавиатурного жеста. В команде Word Wrap свойство IsCheckable обеспечивает
самопереключаемость флажка, а его начальное состояние задается свойством IsChecked.
Теперь добавим к пунктам меню иконки, которые можно взять в архиве VS2008ImageLibrary.zip,
устанавливаемом на компьютер вместе с VS-2008 и находящимся в каталоге Program Files\Microsoft Visual Studio 9.0\Common7\VS2008ImageLibrary\1033.
Мы будем использовать иконки из папки архива VS2008ImageLibrary\Actions\24bitcolor bitmaps.
Для удобства они скопированы в каталог Source данной работы.
В панели Solution
Explorer вызовите контекстное меню для корня проекта Notepad1 и добавьте командой Add/New Folder новую
папку с именем Images
Вызовите для папки Images контекстное меню и командой Add/Existing Item скопируйте в нее
иконки с расширением .bmp из папки Source:
Подключите иконку NewDocument.bmp к
пункту меню File/New, модифицировав разметку для этого элемента такБыло <MenuItem Header="_New" InputGestureText="Ctrl+N">
</MenuItem>
Стало <MenuItem Header="_New" InputGestureText="Ctrl+N">
<MenuItem.Icon>
<Image Source="Images/NewDocument.bmp" Width="16" Height="16" />
</MenuItem.Icon>
</MenuItem>
Запустите приложение
и убедитесь, что иконка появилась слева от названия пункта меню NewЗдесь есть один недостаток, который бросается в глаза - фон иконки имеет цвет Magenta. В Windows Forms фон устраняется установкой параметра ImageTransparentColor в
значение цвета, который система будет считать прозрачным. В WPF для нейтрализации
фона можно применить другой механизм, задействующий параметр OpacityMask, но
пока мы оставим все как есть.
Если в каждый элемент MenuItem мы будем встраивать дескриптор <MenuItem.Icon> с
элементом Image, то разметка сильно разбухнет и станет нечитабельным. Чтобы
сделать код более элегантным, воспользуемся новым механизмом WPF - логическими
ресурсами. Ресурсы хороши тем, что их можно использовать многократно в различных
местах приложения. В нашем приложении одни и те же иконки понадобятся в меню,
панели инструментов и контекстном меню.
Каждый элемент, производный от класса FrameworkElement, включая окно Window,
наследует от FrameworkElement коллекцию Resources типа словаря ресурсов System.Windows. ResourceDictionary. В эту коллекцию
можно помещать описания пар ключ-объект, ссылки на которые потом можно подключать
к элементам управления для встраивания объектов. Такой прием называется расширением
разметки интерфейсных элементов.
Обязательным элементом при объявлении и использовании ресурса является ключ x:Key="идентификатор_ключа", которому можно присваивать произвольное имя.
В пределах одной коллекции ключи должны быть уникальными. Использование ссылки
на ресурс для свойства элемента выполняется по синтаксису
Свойство="{StaticResource идентификатор_ключа}"
или
Свойство="{DynamicResource идентификатор_ключа}"
Синтаксический анализатор разметки XAML присвоит свойству объявленный в ресурсе
объект. Если применяется ссылка на статический ресурс, то объявление ресурса
должно предшествовать его использованию. Встретив ссылку на ресурс анализатор
начинает поиск объекта с заданным ключом вначале в коллекции Resources того
элемента, где расположена ссылка. Не найдя нужную пару анализатор поднимается
на уровень выше и просматривает коллекцию родительского элемента и так далее,
пока не будет найдена соответствующая ссылке пара. Таким образом, ресурсы можно
объявлять в любом элементе, но лучше сосредоточить их в коллекции <Window.Resources> окна
или <Application.Resources> приложения.
Добавьте после открывающего
дескриптора окна объявление логического ресурса, а в самозакрывающемся дескрипторе
элемена меню - его использование по ключу<Window x:Class="Notepad1.Window1"
...............................................
>
<Window.Resources>
<!-- File -->
<Image x:Key="iconNew" Source="Images/NewDocument.bmp" Width="16" Height="16" />
</Window.Resources>
<DockPanel LastChildFill="True">
<!-- Меню -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<!-- Сокращенные варианты подключения иконок с использованием статических ресурсов -->
<MenuItem Header="_New" InputGestureText="Ctrl+N" Icon="{StaticResource iconNew}" />
...............................................
</MenuItem>
...............................................
</Menu>
</DockPanel>
</Window>
Запустите приложение
и убедитесь, что иконка присутствует слева от названия пункта меню New, но
разметка элемента меню стала значительно короче
Расширьте объявление
ресурсов на все иконки<Window.Resources>
<!-- File -->
<Image x:Key="iconNew" Source="Images/NewDocument.bmp" Width="16" Height="16" />
<Image x:Key="iconOpen" Source="Images/Open.bmp" Width="16" Height="16" />
<Image x:Key="iconSave" Source="Images/Save.bmp" Width="16" Height="16" />
<Image x:Key="iconPageSetup" Source="Images/PrintSetup.bmp" Width="16" Height="16" />
<Image x:Key="iconPrintPreview" Source="Images/PrintPreview.bmp" Width="16" Height="16" />
<Image x:Key="iconPrint" Source="Images/Print.bmp" Width="16" Height="16" />
<!-- Edit -->
<Image x:Key="iconUndo" Source="Images/Edit_Undo.bmp" Width="16" Height="16" />
<Image x:Key="iconRedo" Source="Images/Edit_Redo.bmp" Width="16" Height="16" />
<Image x:Key="iconCut" Source="Images/Cut.bmp" Width="16" Height="16" />
<Image x:Key="iconCopy" Source="Images/Copy.bmp" Width="16" Height="16" />
<Image x:Key="iconPaste" Source="Images/Paste.bmp" Width="16" Height="16" />
<Image x:Key="iconDelete" Source="Images/Delete.bmp" Width="16" Height="16" />
<Image x:Key="iconFind" Source="Images/Find.bmp" Width="16" Height="16" />
<Image x:Key="iconFont" Source="Images/Font.bmp" Width="16" Height="16" />
</Window.Resources>
Дополните разметку
создания главного меню ссылками на ресурс объекта рисунка для свойства Icon<!-- Меню -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<!-- Сокращенные варианты подключения иконок с использованием статических ресурсов -->
<MenuItem Header="_New" InputGestureText="Ctrl+N" Icon="{StaticResource iconNew}" />
<MenuItem Header="_Open..." InputGestureText="Ctrl+O" Icon="{StaticResource iconOpen}" />
<MenuItem Header="_Save" InputGestureText="Ctrl+S" Icon="{StaticResource iconSave}" />
<MenuItem Header="Save _As..." />
<Separator />
<MenuItem Header="Page Set_up..." Icon="{StaticResource iconPageSetup}" />
<MenuItem Header="P_rint Preview" InputGestureText="Ctrl+F2" Icon="{StaticResource iconPrintPreview}" />
<MenuItem Header="_Print..." InputGestureText="Ctrl+P" Icon="{StaticResource iconPrint}" />
<Separator />
<MenuItem Header="E_xit" />
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Header="_Undo" InputGestureText="Ctrl+Z" Icon="{StaticResource iconUndo}" />
<MenuItem Header="_Redo" InputGestureText="Ctrl+Y" Icon="{StaticResource iconRedo}" />
<Separator></Separator>
<MenuItem Header="Cu_t" InputGestureText="Ctrl+X" Icon="{StaticResource iconCut}" />
<MenuItem Header="_Copy" InputGestureText="Ctrl+C" Icon="{StaticResource iconCopy}" />
<MenuItem Header="_Paste" InputGestureText="Ctrl+V" Icon="{StaticResource iconPaste}" />
<MenuItem Header="De_lete" InputGestureText="Del" Icon="{StaticResource iconDelete}" />
<Separator></Separator>
<MenuItem Header="_Find..." InputGestureText="Ctrl+F" Icon="{StaticResource iconFind}" />
<MenuItem Header="Find _Next" InputGestureText="F3" />
<MenuItem Header="_Replace..." InputGestureText="Ctrl+H" />
<MenuItem Header="_Go To..." InputGestureText="Ctrl+G" />
<Separator></Separator>
<MenuItem Header="Select _All" InputGestureText="Ctrl+A" />
</MenuItem>
<MenuItem Header="F_ormat">
<MenuItem Header="_Font..." Icon="{StaticResource iconFont}" />
<Separator />
<MenuItem Header="_Word Wrap" IsCheckable="True" IsChecked="True" InputGestureText="Ctrl+W" />
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Header="_About" />
</MenuItem>
</Menu>
Запустите приложение
и проверьте наличие иконок в элементах главного менюПанель инструментов содержит кнопки быстрого доступа к командам меню, наиболее
часто употребляемые пользователем. Обычно она размещается в верхней части рабочей
области окна после меню. Панель инструментов представляет собой как минимум
один контейнер для элементов управления, чаще всего кнопок, определенный классом ToolBar. Но она может состоять и из нескольких
групп инструментов, где каждая группа пакуется в отдельный контейнер ToolBar.
Для управления совместным стандартным поведением нескольких панелей инструментов
все они упаковываются в контейнер ToolBarTray.
Добавьте в контейнер
размещения DockPanel после разметки меню разметку создания панели инструментов<!-- Панель инструментов -->
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<Button Width="23" Content="{StaticResource iconNew}" />
<Button Width="23" Content="{StaticResource iconOpen}" />
<Button Width="23" Content="{StaticResource iconSave}" />
</ToolBar>
<ToolBar>
<Button Width="23" Content="{StaticResource iconUndo}" />
<Button Width="23" Content="{StaticResource iconRedo}" />
<Separator />
<Button Width="23" Content="{StaticResource iconCut}" />
<Button Width="23" Content="{StaticResource iconCopy}" />
<Button Width="23" Content="{StaticResource iconPaste}" />
<Button Width="23" Content="{StaticResource iconDelete}" />
</ToolBar>
<ToolBar Header="Find:">
<TextBox Width="100" />
<Button Width="23" Content="{StaticResource iconFind}" />
</ToolBar>
</ToolBarTray>
В кнопки панели инструментов мы вставляем иконки из ресурсов по тому же самому ключу, который использовали для вставки в пункты меню. То же самое выполним далее и для контекстного меню.
Добавьте после разметки
панели инструментов разметку создания строки состояния<!-- Строка состояния -->
<StatusBar DockPanel.Dock="Bottom" Height="32" Name="statusBar">
<Label>Simulator Application is Loading</Label>
<Separator />
<ProgressBar Height="20" Width="100" IsIndeterminate="True" />
</StatusBar>
Контейнер StatusBar строки состояния мы привязали к нижней части окна и наполнили
тремя элементами, последний из которых будет имитировать ход процесса загрузки
приложения за счет свойства IsIndeterminate="True".
Добавьте после разметки
строки состояния многострочное текстовое поле редактирования TextBox, который
будет представлять рабочую область окна приложения и занимать все свободное
пространство, поскольку в открывающем дескрипторе панели размещения мы указали
для последнего элемента соответствующий параметр <DockPanel LastChildFill="True"><!-- Многострочное текстовое поле редактирования -->
<TextBox TextWrapping="Wrap"
AcceptsReturn="True"
AcceptsTab="True"
VerticalScrollBarVisibility="Auto"
>
</TextBox>
Параметр TextWrapping="Wrap" переносит строку, если она не помещается по ширине
в текстовое поле. AcceptsReturn="True" включает действие клавиши для переноса
строк (accept -
принимать, признавать). AcceptsTab="True" включает действие клавиши табуляции
в текстовом поле. VerticalScrollBarVisibility="Auto" включает автоматическое
появление линейки скролирования, когда текст выходит за пределы области редактирования
по высоте.
Запустите приложение
и вызовите контекстное меню для элемента редактирования щелчком правой кнопкой
мыши или клавиатурным жестом Shift+F10Мы видим, что элемент редактирования имеет по умолчанию контекстное меню, которое называется встроенным. Некоторые элементы управления имеют встроенное контекстное меню с готовой поддержкой команд.
Проверьте, что команды
меню и клавиатурные комбинации выполняют операции вырезания и копирования
в буфер обмена, а также вставку из буфера. Работает также и встроенная логика
отключения источников команд, когда операция невозможна, например, буфер
обмена пуст и нечего вставлятьДля тренировки, в дальнейшем мы переопределим встроенное
в TextBox контекстное меню и встроенные команды, а заодно и отключим клавиатурные
комбинации, инициирующие встроенные команды.
Добавьте внутрь дескриптора TextBox разметку контекстного меню с подключенными пиктограммами для элементов<TextBox.ContextMenu>
<ContextMenu Width="100">
<MenuItem Header="Cu_t" Icon="{StaticResource iconCut}" />
<MenuItem Header="_Copy" Icon="{StaticResource iconCopy}" />
<MenuItem Header="_Paste" Icon="{StaticResource iconPaste}" />
<MenuItem Header="De_lete" Icon="{StaticResource iconDelete}" />
</ContextMenu>
</TextBox.ContextMenu>
Запустите проектОбратите внимание, что применение акселератора Shift+F10 всегда вызывает наше
контекстное меню в центре элемента редактирования.
Отметьте странное поведение
иконок: при открытии разделов меню соответствующая группа иконок исчезает
из других элементов управления. Это происходит потому, что по умолчанию логический
ресурс считается разделяемым. А это значит, что объект, объявленный
в ресурсе, при многократном использовании может появляться только в одном месте.
Чтобы это устранить, нужно объявить ресурс неразделяемым с помощью атрибута x:Shared="False" (share - разделять,
делиться;
shared - общий).
Добавьте в секцию объявления
ресурсов атрибут x:Shared="False"<Window.Resources>
<!-- File -->
<Image x:Shared="False" x:Key="iconNew" Source="Images/NewDocument.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconOpen" Source="Images/Open.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconSave" Source="Images/Save.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPageSetup" Source="Images/PrintSetup.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPrintPreview" Source="Images/PrintPreview.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPrint" Source="Images/Print.bmp" Width="16" Height="16" />
<!-- Edit -->
<Image x:Shared="False" x:Key="iconUndo" Source="Images/Edit_Undo.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconRedo" Source="Images/Edit_Redo.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconCut" Source="Images/Cut.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconCopy" Source="Images/Copy.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPaste" Source="Images/Paste.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconDelete" Source="Images/Delete.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconFind" Source="Images/Find.bmp" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconFont" Source="Images/Font.bmp" Width="16" Height="16" />
</Window.Resources>
Запустите проект -
убедитесь, что сам интерфейс функционирует нормальноИ наконец, прежде чем приступить к кодированию логики, все-таки изменим фон
иконок, просто заменив их на рисунки с прозрачным фоном. Такой набор иконок
можно найти все в том же поставляемом вместе с Visual Studio 2008 архиве VS2008ImageLibrary.zip в папке VS2008ImageLibrary\Actions\pngformat.
Для удобства этот набор скопирован в прилагаемую к работе папку Source. Это
файлы типа png и их имена имеют приставку HS (Hide or Show), например, вместо Cut.bmp имя нового файла - CutHS.png.
Добавьте в папку Images проекта из Source 14 рисунков png командой контекстного меню Add/Existing Item
Командой Find and
Replace (жест Ctrl+H) выполните замену в файле Window1.xaml всех
вхождений .bmp на HS.png
Запустите проект и
убедитесь, что фон иконок во всех элементах стал прозрачным
Прежде, чем приступить к реализации функциональности с помощью подключения
команд, потренируемся на отключении встроенных команд элемента TextBox. Обратите
внимание, что хоть мы и заменили встроенное меню TextBox на свое и соответствующие
опции в этом контекстном меню перестали работать, все-таки продолжают функционировать
встроенные жесты, типичные для текстового поля редактирования: Ctrl+Home, Ctrl+End,
Ctrl+X, Ctrl+C, Ctrl+V, Ctrl+Z, Ctrl+Y и т.д. Попробуем отключить встроенные
жесты на примере команд Cut (Ctrl+X), Copy (Ctrl+C) и Paste (Ctrl+V).
Добавьте в открывающий
дескриптор окна подписку на событие Loaded, чтобы создать обработчик для
размещения процедурного кода, который будет выполнен сразу после инициализации
элементов окна, определенных в разметке<Window x:Class="Notepad1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
>
......................................................
</Window>
Присвойте в разметке
имя элементу TextBox для возможности его кодирования в файле Window1.xaml.cs<TextBox TextWrapping="Wrap"
AcceptsReturn="True"
AcceptsTab="True"
VerticalScrollBarVisibility="Auto"
Name="txtBox1"
>
.................................................
</TextBox>
Добавьте в обработчик
события Loaded следующий код отключения встроенных жестов вырезания, копирования
и вставки в элементе текстового поляprivate void Window_Loaded(object sender, RoutedEventArgs e)
{
// Отключаем в TextBox встроенный жест Ctrl+X для команды Cut
KeyBinding keyBinding = new KeyBinding(
ApplicationCommands.NotACommand, Key.X, ModifierKeys.Control);
txtBox1.InputBindings.Add(keyBinding);
// Отключаем в TextBox встроенный жест Ctrl+C для команды Copy
keyBinding = new KeyBinding(
ApplicationCommands.NotACommand, Key.C, ModifierKeys.Control);
txtBox1.InputBindings.Add(keyBinding);
// Отключаем в TextBox встроенный жест Ctrl+V для команды Paste
keyBinding = new KeyBinding(
ApplicationCommands.NotACommand, Key.V, ModifierKeys.Control);
txtBox1.InputBindings.Add(keyBinding);
}
Ключевым элементом в данном способе отключения жестов является команда NotACommand, которая означает пустую команду. Мы отключили только источники
возбуждения встроенных команд для конкретного элемента TextBox, но сами встроенные
команды продолжают существовать с теми же самыми жестами.
Скопируйте из папки Source в корень проекта Notepad1 командой контекстного меню Add/Existing Item иконку Notepad.ico и подключите ее к окну Window1<Window x:Class="Notepad1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
>
......................................................
</Window>
Код решения поставленной задачи будет достаточно большим. Чтобы сделать его обозримым, распределим отдельные группы частичного класса по отдельным файлам в соответствии с разделами меню. Все равно компилятор их увидет и соберет в единую сборку.
В панели Solution
Explorer выделите узел проекта Notepad1 и командой Project/Add Class добавьте
три файла с именами File.cs, Edit.cs и Other.cs
Скопируйте из файла Window1.xaml.cs в каждый из этих новых файлов код подключения пространств
имен и отредактируйте заготовки частей класса Window1, которые пока будут
одинаковые, такusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
}
}
Откройте файл Window1.xaml.cs и дополните класс Window1 вспомогательным кодом, после чего он должен стать
такимusing System;
using System.Windows;
using System.Windows.Input;
using Microsoft.Win32; // Для стандартных диалогов Win32
using System.IO; // Работа с файлами и каталогами
namespace Notepad1
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Отключаем в TextBox встроенный жест Ctrl+X для команды Cut
KeyBinding keyBinding = new KeyBinding(
ApplicationCommands.NotACommand, Key.X, ModifierKeys.Control);
txtBox1.InputBindings.Add(keyBinding);
// Отключаем в TextBox встроенный жест Ctrl+C для команды Copy
keyBinding = new KeyBinding(
ApplicationCommands.NotACommand, Key.C, ModifierKeys.Control);
txtBox1.InputBindings.Add(keyBinding);
// Отключаем в TextBox встроенный жест Ctrl+V для команды Paste
keyBinding = new KeyBinding(
ApplicationCommands.NotACommand, Key.V, ModifierKeys.Control);
txtBox1.InputBindings.Add(keyBinding);
}
#region private Fields - локальные поля
//------------------------------------------------------
//
// private Fields - локальные поля
//
//------------------------------------------------------
bool IsModified = false;// Флаг изменений содержимого
string strLoadedFile; // Полное имя загруженного документа
#endregion private Fields
#region Auxiliary Methods - вспомогательные методы
//------------------------------------------------------
//
// Auxiliary Methods - вспомогательные методы
//
//------------------------------------------------------
// Метод возвращает true, если содержимое
// TextBox не требует сохранения
bool flag;
bool CheckModifiedAndSaveIt()
{
if (!IsModified)
return true;
MessageBoxResult result =
MessageBox.Show(
"Сохранить изменения?", "", // Контекст и заголовок
MessageBoxButton.YesNoCancel, // Кнопки диалога
MessageBoxImage.Question, // Иконка вопроса
MessageBoxResult.Yes // Кнопка с фокусом
);
switch (result)
{
case MessageBoxResult.Yes:
if (String.IsNullOrEmpty(strLoadedFile))
flag = DisplaySaveDialog(""); // Запись с диалогом
else
flag = SaveFile(strLoadedFile); // Просто запись
break;
case MessageBoxResult.No:
flag = true;
break;
case MessageBoxResult.Cancel:
flag = false;
break;
}
return flag;
}
// Вызывает диалоговое окно записи файла
// и возвращает true, если файл был сохранен
bool DisplaySaveDialog(string strFileName)
{
SaveFileDialog dlg = new SaveFileDialog();
dlg.Filter = "Text Documents(*.txt)|*.txt|All Files(*.*)|*.*";
dlg.FileName = strFileName;
bool result = (bool)dlg.ShowDialog(this); // Желание пользователя
if (result)
result = SaveFile(dlg.FileName); // Возможность компьютера
return result;
}
// Сохраняет документ и возвращает true при успехе
// Аргумент - полное имя файла
bool SaveFile(string strFileName)
{
try
{
File.WriteAllText(strFileName, txtBox1.Text,
System.Text.Encoding.GetEncoding(1251));
}
catch (Exception e)
{
// Ловим все исключения и выводим диалог
MessageBox.Show(
"Ошибка записи файла:\n" + e.Message, "",
MessageBoxButton.OK,
MessageBoxImage.Asterisk
);
return false;
}
strLoadedFile = strFileName;
UpdateTitle(); // Меняем заголовок окна
IsModified = false; // Нет изменений текста
return true;
}
// Диалог открытия файла возвращает true при успехе
bool DisplayOpenDialog()
{
flag = CheckModifiedAndSaveIt(); // Проверяем и сохраняем изменения
if(!flag)
return flag;
OpenFileDialog dlg = new OpenFileDialog();
dlg.Filter = "Text Documents(*.txt)|*.txt|All Files(*.*)|*.*";
bool result = (bool)dlg.ShowDialog(this); // Желание пользователя
if (result)
result = OpenFile(dlg.FileName); // Возможность компьютера
return result;
}
// Открывает файл и при успехе возвращает true
bool OpenFile(string strFileName)
{
try
{
txtBox1.Text = File.ReadAllText(strFileName,
System.Text.Encoding.GetEncoding(1251));
}
catch (Exception e)
{
// Ловим все исключения и выводим диалог
MessageBox.Show(
"Ошибка чтения файла:\n" + e.Message, "",
MessageBoxButton.OK,
MessageBoxImage.Asterisk
);
return false;
}
strLoadedFile = strFileName;
UpdateTitle(); // Меняем заголовок окна
IsModified = false; // Нет изменений текста
// Сбрасываем границы выделенного текста поля редактирования
txtBox1.SelectionStart = 0;
txtBox1.SelectionLength = 0;
return true;
}
// Коррекция заголовка окна
void UpdateTitle()
{
// Извлекаем заголовок окна из словаря ресурсов
String title = Application.Current.
Resources["ApplicationTitle1"].ToString();
//if (strLoadedFile == null || strLoadedFile.Trim() == String.Empty)
if (String.IsNullOrEmpty(strLoadedFile)) // Проще!
{
this.Title = "Untitled - " + title;
return;
}
// Извлекаем имя файла из полного пути
int startIndex = strLoadedFile.LastIndexOf('\\') + 1;
int endIndex;
// Проверяем в системном реестре настройки системы по скрытию расширения файлов
using (RegistryKey filekey = Registry.CurrentUser.CreateSubKey(
@"Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"))
{
if ((filekey != null) (filekey.GetValue("HideFileExt", 0).ToString() == "0"))
{
endIndex = strLoadedFile.Length; // Нет расширения
}
else
{
endIndex = strLoadedFile.LastIndexOf('.'); // Отсекаем расширение
}
}
if (endIndex > startIndex)
{
this.Title = strLoadedFile.Substring(startIndex) +
" - " + title;
}
else
{
this.Title = strLoadedFile.Substring(startIndex, endIndex - startIndex) +
" - " + title;
}
}
#endregion Auxiliary Methods
}
}
Смысл добавленного кода подробно помечен коментариями и следует его внимательно
изучить. Ключевым полем работы кода является флаг IsModified, сигнализирующий
об изменении содержимого элемента TextBox. Поднятие флага выполним в обработчике
события TextChanged этого элемента.
В разметку TextBox добавьте регистрацию обработчика события TextChanged и создайте сам обработчик<!-- Многострочное текстовое поле редактирования -->
<TextBox TextWrapping="Wrap"
AcceptsReturn="True"
AcceptsTab="True"
VerticalScrollBarVisibility="Auto"
Name="txtBox1"
TextChanged="txtBox1_TextChanged"
>
....................................................
</TextBox>
Обработчик txtBox1_TextChanged() заполните
такprivate void txtBox1_TextChanged(object sender,
System.Windows.Controls.TextChangedEventArgs e)
{
if (IsModified)
return;
else
IsModified = true;
}
В коде вспомогательных функций используется словарь ресурсов приложения, которые мы сейчас определим.
Откройте файл разметки App.xaml и дополните его определением ресурсов для хранения неизменяемой части
заголовка окна. Областью видимости этих ресурсов будет все приложение<Application x:Class="Notepad1.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml">
<Application.Resources>
<String xmlns="clr-namespace:System;assembly=Mscorlib" x:Key="ApplicationTitle1">
Title="Window1: Управление состоянием источников команд"
</String>
<String xmlns="clr-namespace:System;assembly=Mscorlib" x:Key="ApplicationTitle2">
Title="Window2: Управление состоянием источников команд"
</String>
</Application.Resources>
</Application>
Будем мы использовать механизм команд WPF или нет, но без событий и их обработчиков
никак не обойтись. Начнем с того факта, что для решения наших задач можно создать
обработчики с одинаковой сигнатурой, определяемой делегатом пространства имен System.Windows:
public delegate void RoutedEventHandler(object sender, RoutedEventArgs e)
Имена обработчикам будем присваивать в соответствии с решаемой ими задачей.
Обработчики создадим вручную и их необязательно сразу присоединять к событиям
источников, поскольку неприсоединенные обработчики будут считаться обычными
методами класса. Наиболее полно все задачи представлены в главном меню приложения,
поэтому для каждой из них нужно создать свою группу обработчиков. Выполним
все это по порядку, размещая группы обработчиков в соответствующих частях класса Window1.
Добавьте в часть класса Window1, расположенную в файле File.cs, следующие пустые обработчики
по количеству задач меню в разделе Fileusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Обработчики источников задач File
//
//------------------------------------------------------
private void NewOnExecute(object sender, RoutedEventArgs e)
{
}
private void OpenOnExecute(object sender, RoutedEventArgs e)
{
}
private void SaveOnExecute(object sender, RoutedEventArgs e)
{
}
private void SaveAsOnExecute(object sender, RoutedEventArgs e)
{
}
private void PageSetupOnExecute(object sender, RoutedEventArgs e)
{
}
private void PrintPreviewOnExecute(object sender, RoutedEventArgs e)
{
}
private void PrintOnExecute(object sender, RoutedEventArgs e)
{
}
private void ExitOnExecute(object sender, RoutedEventArgs e)
{
}
}
}
Добавьте в часть класса Window1, расположенную в файле Edit.cs, следующие пустые обработчики
по количеству задач меню в разделе Editusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Обработчики источников задач Edit
//
//------------------------------------------------------
private void UndoOnExecute(object sender, RoutedEventArgs e)
{
}
private void RedoOnExecute(object sender, RoutedEventArgs e)
{
}
private void CutOnExecute(object sender, RoutedEventArgs e)
{
}
private void CopyOnExecute(object sender, RoutedEventArgs e)
{
}
private void PasteOnExecute(object sender, RoutedEventArgs e)
{
}
private void DeleteOnExecute(object sender, RoutedEventArgs e)
{
}
private void FindOnExecute(object sender, RoutedEventArgs e)
{
}
private void FindNextOnExecute(object sender, RoutedEventArgs e)
{
}
private void ReplaceOnExecute(object sender, RoutedEventArgs e)
{
}
private void GoToOnExecute(object sender, RoutedEventArgs e)
{
}
private void SelectAllOnExecute(object sender, RoutedEventArgs e)
{
}
}
}
Добавьте в часть класса Window1, расположенную в файле Other.cs, следующие пустые обработчики
по количеству задач меню в оставшихся разделахusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Прочие обработчики
//
//------------------------------------------------------
private void FontOnExecute(object sender, RoutedEventArgs e)
{
}
private void WordWrapOnExecute(object sender, RoutedEventArgs e)
{
}
private void AboutOnExecute(object sender, RoutedEventArgs e)
{
}
}
}
Запустите приложение
и убедитесь, что ошибок компиляции нет, но функциональности от выполненной
нами работы в нем пока не прибавилосьМы подготовили интерфейс окна Window1 и дальше наступает этап кодирования
функциональности. В окне Window1 мы откажемся от услуг механизма команд, а будем использовать
обычную технологию, основанную на обработчиках событий. Начнем постепенно подключать
обработчики к источникам задач, используя событие Click.
Подключите декларативно
в файле разметки Window1.xaml к каждому источнику события Click соответствующий
обработчик и назначьте имена элементам для последующего управления ими из
кодаОбратите внимание на некоторую некорректность работы подсказчика
кода IntelliSense, который не предлагает нам список уже созданных обработчиков.
Он ищет их в застраничном файле Window1.xaml.cs, в то время как обработчики
находятся в других файлах частичного класса. В данном случае это не слишком
большое неудобство, поскольку мнемоника составления имен у нас строго соблюдена
и мы вряд ли ошибемся. Но в больших практических проектах это следует иметь
ввиду, прежде чем разбивать класс окна по отдельным файлам.
Разметка с подключенными обработчиками и присвоенными именами будет выглядеть так (файл приводится целиком)
<Window x:Class="Notepad1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
>
<Window.Resources>
<!-- File -->
<Image x:Shared="False" x:Key="iconNew" Source="Images/NewDocumentHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconOpen" Source="Images/OpenHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconSave" Source="Images/SaveHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPageSetup" Source="Images/PrintSetupHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPrintPreview" Source="Images/PrintPreviewHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPrint" Source="Images/PrintHS.png" Width="16" Height="16" />
<!-- Edit -->
<Image x:Shared="False" x:Key="iconUndo" Source="Images/Edit_UndoHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconRedo" Source="Images/Edit_RedoHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconCut" Source="Images/CutHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconCopy" Source="Images/CopyHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPaste" Source="Images/PasteHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconDelete" Source="Images/DeleteHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconFind" Source="Images/FindHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconFont" Source="Images/FontHS.png" Width="16" Height="16" />
</Window.Resources>
<DockPanel LastChildFill="True">
<!-- Меню -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<!-- Сокращенные варианты подключения иконок с использованием статических ресурсов -->
<MenuItem Name="itemNew" Click="NewOnExecute" Header="_New" InputGestureText="Ctrl+N"
Icon="{StaticResource iconNew}" />
<MenuItem Name="itemOpen" Click="OpenOnExecute" Header="_Open..." InputGestureText="Ctrl+O"
Icon="{StaticResource iconOpen}" />
<MenuItem Name="itemSave" Click="SaveOnExecute" Header="_Save" InputGestureText="Ctrl+S"
Icon="{StaticResource iconSave}" />
<MenuItem Name="itemSaveAs" Click="SaveAsOnExecute" Header="Save _As..." />
<Separator />
<MenuItem Name="itemPageSetup" Click="PageSetupOnExecute"
Header="Page Set_up..." Icon="{StaticResource iconPageSetup}" />
<MenuItem Name="itemPrintPreview" Click="PrintPreviewOnExecute" Header="P_rint Preview"
InputGestureText="Ctrl+F2" Icon="{StaticResource iconPrintPreview}" />
<MenuItem Name="itemPrint" Click="PrintOnExecute"
Header="_Print..." InputGestureText="Ctrl+P" Icon="{StaticResource iconPrint}" />
<Separator />
<MenuItem Name="itemExit" Click="ExitOnExecute" Header="E_xit" />
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Name="itemUndo" Click="UndoOnExecute"
Header="_Undo" InputGestureText="Ctrl+Z" Icon="{StaticResource iconUndo}" />
<MenuItem Name="itemRedo" Click="RedoOnExecute"
Header="_Redo" InputGestureText="Ctrl+Y" Icon="{StaticResource iconRedo}" />
<Separator></Separator>
<MenuItem Name="itemCut" Click="CutOnExecute"
Header="Cu_t" InputGestureText="Ctrl+X" Icon="{StaticResource iconCut}" />
<MenuItem Name="itemCopy" Click="CopyOnExecute"
Header="_Copy" InputGestureText="Ctrl+C" Icon="{StaticResource iconCopy}" />
<MenuItem Name="itemPaste" Click="PasteOnExecute"
Header="_Paste" InputGestureText="Ctrl+V" Icon="{StaticResource iconPaste}" />
<MenuItem Name="itemDelete" Click="DeleteOnExecute"
Header="De_lete" InputGestureText="Del" Icon="{StaticResource iconDelete}" />
<Separator></Separator>
<MenuItem Name="itemFind" Click="FindOnExecute"
Header="_Find..." InputGestureText="Ctrl+F" Icon="{StaticResource iconFind}" />
<MenuItem Name="itemFindNext" Click="FindNextOnExecute"
Header="Find _Next" InputGestureText="F3" />
<MenuItem Name="itemReplace" Click="ReplaceOnExecute"
Header="_Replace..." InputGestureText="Ctrl+H" />
<MenuItem Name="itemGoTo" Click="GoToOnExecute"
Header="_Go To..." InputGestureText="Ctrl+G" />
<Separator></Separator>
<MenuItem Name="itemSelectAll" Click="SelectAllOnExecute"
Header="Select _All" InputGestureText="Ctrl+A" />
</MenuItem>
<MenuItem Header="F_ormat">
<MenuItem Name="itemFont" Click="FontOnExecute"
Header="_Font..." Icon="{StaticResource iconFont}" />
<Separator />
<MenuItem Name="itemWordWrap" Click="WordWrapOnExecute"
Header="_Word Wrap" IsCheckable="True" IsChecked="True" InputGestureText="Ctrl+W" />
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Name="itemAbout" Click="AboutOnExecute"
Header="_About" />
</MenuItem>
</Menu>
<!-- Панель инструментов -->
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<Button Name="btnNew" Click="NewOnExecute" Width="23" Content="{StaticResource iconNew}" />
<Button Name="btnOpen" Click="OpenOnExecute" Width="23" Content="{StaticResource iconOpen}" />
<Button Name="btnSave" Click="SaveOnExecute" Width="23" Content="{StaticResource iconSave}" />
</ToolBar>
<ToolBar>
<Button Name="btnUndo" Click="UndoOnExecute" Width="23" Content="{StaticResource iconUndo}" />
<Button Name="btnRedo" Click="RedoOnExecute" Width="23" Content="{StaticResource iconRedo}" />
<Separator />
<Button Name="btnCut" Click="CutOnExecute" Width="23" Content="{StaticResource iconCut}" />
<Button Name="btnCopy" Click="CopyOnExecute" Width="23" Content="{StaticResource iconCopy}" />
<Button Name="btnPaste" Click="PasteOnExecute" Width="23" Content="{StaticResource iconPaste}" />
<Button Name="btnDelete" Click="DeleteOnExecute" Width="23" Content="{StaticResource iconDelete}" />
</ToolBar>
<ToolBar Header="Find:">
<TextBox Width="100" />
<Button Name="btnFind" Click="FindOnExecute" Width="23" Content="{StaticResource iconFind}" />
</ToolBar>
</ToolBarTray>
<!-- Строка состояния -->
<StatusBar DockPanel.Dock="Bottom" Height="32" Name="statusBar">
<Label>Simulator Application is Loading</Label>
<Separator />
<ProgressBar Height="20" Width="100" IsIndeterminate="True" />
</StatusBar>
<!-- Многострочное текстовое поле редактирования -->
<TextBox TextWrapping="Wrap"
AcceptsReturn="True"
AcceptsTab="True"
VerticalScrollBarVisibility="Auto"
Name="txtBox1"
>
<TextBox.ContextMenu>
<ContextMenu Width="100">
<MenuItem Name="contextCut" Click="CutOnExecute"
Header="Cu_t" Icon="{StaticResource iconCut}" />
<MenuItem Name="contextCopy" Click="CopyOnExecute"
Header="_Copy" Icon="{StaticResource iconCopy}" />
<MenuItem Name="contextPaste" Click="PasteOnExecute"
Header="_Paste" Icon="{StaticResource iconPaste}" />
<MenuItem Name="contextDelete" Click="DeleteOnExecute"
Header="De_lete" Icon="{StaticResource iconDelete}" />
</ContextMenu>
</TextBox.ContextMenu>
</TextBox>
</DockPanel>
</Window>
Столько имен нам не понадобиться, но для единообразия мы, на всякий случай, промаркировали все источники задач (может пригодится).
Запустите приложение
для проверки отсутствия синтаксических ошибокСледующим шагом следует наполнить созданные заготовки обработчиков практическим кодом в соответствии с логикой работы приложения. Поскольку наша задача состоит не в создании полнофункционального приложения целиком, а в иллюстрации технологии его создания с применением механизма команд, реализуем намеченные задачи частично. Некоторые функции, ввиду их большого объема и сложности кодирования, мы только обозначим выдачей соответствующих диалоговых окон или сообщений. Управление доступностью элементов-источников задач пользовательского интерфейса пока отложим 'на потом' (или навсегда!).
Выделите корень проекта Notepad1 и командой Project/Add Reference подключите сборки System.Windows.Forms.dll и System.Drawing.dll
Модифицируйте код файла File.cs следующим образомusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
// Для сборок: System.Windows.Forms.dll, System.Drawing.dll
using sdp = System.Drawing.Printing;// Псевдоним пространства имен
using swf = System.Windows.Forms; // Псевдоним пространства имен
using PageSetupDialog = System.Windows.Forms.PageSetupDialog;// Псевдоним класса
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Обработчики источников задач File
//
//------------------------------------------------------
private void NewOnExecute(object sender, RoutedEventArgs e)
{
// Пользователь передумал или была ошибка записи изменений
if (!CheckModifiedAndSaveIt())
return;
// Изменений нет или они успешно сохранены
//txtBox1.Text = String.Empty; // Вариант I
//txtBox1.Text = ""; // Вариант II
txtBox1.Clear(); // Вариант III
strLoadedFile = null;
IsModified = false;
UpdateTitle();
txtBox1.Focus();
}
private void OpenOnExecute(object sender, RoutedEventArgs e)
{
if(DisplayOpenDialog())
txtBox1.CaretIndex = txtBox1.Text.Length;// Курсор в конец
txtBox1.Focus();// Передача фокуса
}
private void SaveOnExecute(object sender, RoutedEventArgs e)
{
if (String.IsNullOrEmpty(strLoadedFile))
DisplaySaveDialog(String.Empty);
else
SaveFile(strLoadedFile);
txtBox1.Focus();
}
private void SaveAsOnExecute(object sender, RoutedEventArgs e)
{
DisplaySaveDialog(strLoadedFile);
txtBox1.Focus();
}
private void PageSetupOnExecute(object sender, RoutedEventArgs e)
{
// Ограничемся только показом окна Windows Forms
PageSetupDialog dlg = new PageSetupDialog();
// Без настроек не работает. Зададим хотя бы по умолчанию
dlg.PageSettings = new sdp.PageSettings();
dlg.PrinterSettings = new sdp.PrinterSettings();
dlg.ShowDialog();
txtBox1.Focus();
}
private void PrintPreviewOnExecute(object sender, RoutedEventArgs e)
{
sdp.PrintDocument document = new sdp.PrintDocument();
document.DocumentName = strLoadedFile;
swf.PrintPreviewDialog dlg = new swf.PrintPreviewDialog();
dlg.Document = document;
dlg.UseAntiAlias = true;// Включить сглаживание
dlg.ShowDialog();
txtBox1.Focus();
}
private void PrintOnExecute(object sender, RoutedEventArgs e)
{
sdp.PrintDocument document = new sdp.PrintDocument();
document.DocumentName = strLoadedFile;
swf.PrintDialog dlg = new swf.PrintDialog();
dlg.Document = document;
dlg.ShowDialog();
txtBox1.Focus();
}
private void ExitOnExecute(object sender, RoutedEventArgs e)
{
if (!CheckModifiedAndSaveIt())
return; // Пользователь передумал выходить
Close();
}
}
}
Обратите внимание на использование псевдонимов пространств имен добавленных
к проекту библиотечных сборок. Еще раз вспомним, что при совместном применении
пользовательских интерфейсов WPF и Windows Forms имена типов одной из
технологий следует прописывать полностью или использовать псевдонимы. Иначе
в коде могут возникнуть конфликты имен, а если компилятор их не обнаружит,
то и серьезные ошибки времени выполнения.
Запустите приложение,
испытайте правильность функционирования обработчиков, изучите их кодПока в приложение закрался один существенный недостаток. Когда документ содержит
несохраненные изменения и пользователь завершает работу приложения по нашей
команде Exit, то все в порядке - приложение извещает о необходимости их сохранить.
Но когда окно закрывается по системной кнопке, то извещение отсутствует. Исправим
это, для чего воспользуемся событием Closing. В отличии от Closing событие Closed возбудается, когда отменить закрытие окна уже невозможно, а можно
только что-то
доделать.
Присоедините к открывающему
дескриптору окна Window1 в файле Window1.xaml обработчик события Closing,
которое будет возбуждаться при попытке закрытия окна любым способом<Window x:Class="Notepad1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
Closing="Window_Closing"
>
Заполните обработчик
события Closing следующим кодомprivate void Window_Closing(object sender,
System.ComponentModel.CancelEventArgs e)
{
if (!CheckModifiedAndSaveIt())
{
e.Cancel = true;
return; // Пользователь передумал выходить
}
}
Запустите приложение
- теперь сообщение о несохраненных изменениях выводится и при попытке закрытия
окна системной кнопкойОбратите внимание, что при закрытии окна через наше меню задачей Exit на
кнопку отказа от сохранения надо щелкать 2 раза. Это связано с тем, что в обработчике
события Closing проверка повторяется. Значит при выходе через меню
проверку в обработчике события Closing нужно блокировать с помощью флага.
Добавьте в класс Window1 поле-флаг _IsExitItem и модифицируйте соответствующим образом обработчикиbool _IsExitItem = false;
private void ExitOnExecute(object sender, RoutedEventArgs e)
{
if (!CheckModifiedAndSaveIt())
return; // Пользователь передумал выходить
_IsExitItem = true;
Close();
}
private void Window_Closing(object sender,
System.ComponentModel.CancelEventArgs e)
{
/*
// Эта проверка была бы надежнее
if(_IsExitItem)
return;
*/
// !_IsExitItem должен в условии стоять первым
if (!_IsExitItem !CheckModifiedAndSaveIt())
{
e.Cancel = true;
_IsExitItem = false;
return; // Пользователь передумал выходить
}
}
В последнем обработчике есть один поучительный нюанс: если в условии проверку
значения флага поставить последним, то код будет реагировать на наши нововведения.
Это происходит потому, что логическое умножение проверяется компилятором слева
направо до первого ложного значения. В нашем случае если флаг _IsExitItem==true,
то функция CheckModifiedAndSaveIt() выполняться уже не будет. Такой код менее
надежен, поскольку зависит от компилятора, да и мы (или сопровождающий программист)
можем случайно переставить члены выражения местами. Поэтому лучше заменить
этот код на более ясный, как показано в коментариях.
Запустите приложение
и испытайте работу закрытия окна при несохраняемых измененияхЗадачи раздела меню Edit, связанные с использованием буфера обмена, реализовать
достаточно легко, поскольку вся необходимая функциональность уже заложена в
элементе TextBox. Одними из первых подключим откаты Undo и Redo. Можно было
бы создать свой механизм хранения откатов, но мы воспользуемся встроенным в TextBox журналом откатов. Он поддерживается методами Undo() и Redo(). Все члены
класса TextBox можно посмотреть в MSDN на русском языке по ссылке
http://msdn.microsoft.com/ru-ru/library/system.windows.controls.textbox_members.aspx
Еще раз вспомним, что мы пока не управляем доступностью элементов-источников задач.
В файле Edit.cs заполните
часть обработчиков следующим кодомpartial class Window1
{
//------------------------------------------------------
//
// Обработчики источников задач Edit
//
//------------------------------------------------------
private void UndoOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Undo();
}
private void RedoOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Redo();
}
private void CutOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Cut();
// Вариант
//Clipboard.SetText(txtBox1.SelectedText);
//txtBox1.SelectedText = "";
}
private void CopyOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Copy();
// Вариант
//Clipboard.SetText(txtBox1.SelectedText);
}
private void PasteOnExecute(object sender, RoutedEventArgs e)
{
// Если в буфере содержатся данные текстового формата
if (Clipboard.ContainsText())
txtBox1.Paste();
}
private void DeleteOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.SelectedText = String.Empty;
}
private void FindOnExecute(object sender, RoutedEventArgs e)
{
}
private void FindNextOnExecute(object sender, RoutedEventArgs e)
{
}
private void ReplaceOnExecute(object sender, RoutedEventArgs e)
{
}
private void GoToOnExecute(object sender, RoutedEventArgs e)
{
}
private void SelectAllOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.SelectAll();
}
}
Оставшиеся обработчики задач меню Edit требуют более сложного кода с использованием
немодальных диалоговых окон, которые должны располагаться всегда поверх основного
окна с редактируемым текстом. Попробуем последовательно реализовать их.
Создадим диалоговое окно и назначим ему в качестве владельца основное окно Window1. Окно, имеющее владельца, всегда располагается поверх него, свертывается
и закрывается вместе с ним. Именно так и должно вести себя проектируемое окно Find and Replace в режиме runtime (при выполнении).
В панели Solution
Explorer выделите узел проекта Notepad1 и командой меню Project/Add Window добавьте новое окно с именем FindAndReplaceDialog
Заполните файл FindAndReplaceDialog.xaml разметкой создания интерфейса диалогового окна<Window x:Class="Notepad1.FindAndReplaceDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Find and Replace"
WindowStartupLocation="CenterOwner"
SizeToContent="WidthAndHeight"
ResizeMode="NoResize"
ShowInTaskbar="false"
Activated="OnActivated"
Background="{StaticResource DialogBackgroundBrush}"
>
<Grid ShowGridLines="False" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Контейнер для кнопок -->
<StackPanel
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="2"
Margin="5"
>
<!-- Задаем отступы кнопок -->
<StackPanel.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="Margin" Value="3" />
</Style>
</StackPanel.Resources>
<Button
MinWidth="75"
MinHeight="23"
Name="_findNext"
IsDefault="True"
IsEnabled="False"
Click="FindNextClicked"
Content="_Find Next" />
<Button
MinWidth="75"
MinHeight="23"
Name="_replace"
Visibility="Collapsed"
Click="ReplaceClicked"
Content="_Replace" />
<Button
MinWidth="75"
MinHeight="23"
Name="_replaceAll"
Click="ReplaceAllClicked"
Visibility="Collapsed"
Content="Replace _All" />
<Button
MinWidth="75"
MinHeight="23"
Click="CancelClicked"
IsCancel="True"
Content="Cancel" />
</StackPanel>
<!-- Текстовые метки и текстовые поля в левом верхнем квадранте -->
<Grid
ShowGridLines="False"
Grid.Row="0"
Grid.Column="0"
Margin="5 10 0 0" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Label
Grid.Row="0"
Grid.Column="0"
VerticalAlignment="Center"
Target="{Binding ElementName=_findWhat}"
Content="Fi_nd what:" />
<TextBox
Name="_findWhat"
Grid.Row="0"
Grid.Column="1"
Width="150"
MaxLength="100"
Margin="0 5 0 5"
AcceptsReturn="False"
TextChanged="FindTextChanged" />
<Label
Name="_replaceLabel"
Grid.Row="1"
Grid.Column="0"
VerticalAlignment="Center"
Target="{Binding ElementName=_replaceWith}"
Visibility="Collapsed"
Content="Re_place with:" />
<TextBox
Name="_replaceWith"
Grid.Row="1"
Grid.Column="1"
Width="150"
MaxLength="100"
VerticalAlignment="Center"
Visibility="Collapsed"
AcceptsReturn="False" />
</Grid>
<!-- Секция установки регистра и направления поиска для замены -->
<Grid
Grid.Row="1"
Grid.Column="0"
>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!-- Установка регистра -->
<CheckBox
Name="_matchCase"
Grid.Column="0"
Margin="8 0 10 10"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
IsChecked="False"
Focusable="True"
Content="Match _case" />
<!-- Группа радиокнопок направления поиска -->
<GroupBox
Name="_directionGroupBox"
Header="Direction"
Grid.Column="1"
HorizontalAlignment="Right"
Margin="0 10 0 10">
<StackPanel Orientation="Horizontal" Margin="10 20 10 10">
<RadioButton
Name="_findDown"
IsChecked="True"
Content="_Down" />
<RadioButton
Name="_findUp"
Margin="8 0 0 0"
Content="_Up" />
</StackPanel>
</GroupBox>
</Grid>
</Grid>
</Window>
В настройках окна мы устанавливаем цвет фона из статического ресурса параметром
Background="{StaticResource DialogBackgroundBrush}"
Обратите внимание и
на другие настройки интерфейса окна и элементов
Добавьте в ресурсы
приложения файла App.xaml следующий код<Application x:Class="Notepad1.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="FindAndReplaceDialog.xaml">
<Application.Resources>
<String xmlns="clr-namespace:System;assembly=Mscorlib" x:Key="ApplicationTitle1">
"Window1: Управление состоянием источников команд"
</String>
<String xmlns="clr-namespace:System;assembly=Mscorlib" x:Key="ApplicationTitle2">
"Window2: Управление состоянием источников команд"
</String>
<SolidColorBrush x:Key="DialogBackgroundBrush"
Color="{x:Static SystemColors.ControlColor}" />
</Application.Resources>
</Application>
Обратите внимание, что временно для просмотра интерфейса в режиме выполнения
мы назначили диалоговое окно стартовым окном приложения. Чуть позже мы вернем
класс Window1 в категорию стартовых, а пока нам нужно просто посмотреть, как
выглядит созданный интерфейс.
Запустите приложение Notepad1 - начальный интерфейс для задачи Find должен быть таким
В настройках элементов разметки, связанных с задачей Replace, мы использовали
значение атрибута Visibility=" Collapsed ". Есть еще значения Visibility=" Hidden " и
Visibility=" Visible " (по умолчанию). Hidden - элемент скрыт
(не отображается, но занимает место), Collapsed - элемент свернут (не отображается
и не занимает место).
Заполните кодовую часть
в файле FindAndReplaceDialog.xaml.cs следующим образом (...следующим 'светлым
образом' - шутка!)using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Notepad1
{
public partial class FindAndReplaceDialog : Window
{
public FindAndReplaceDialog()
{
InitializeComponent();
// Начальная доступность кнопок, хотя кнопки Replece
_findNext.IsEnabled = _replace.IsEnabled = _replaceAll.IsEnabled
= !String.IsNullOrEmpty(_findWhat.Text);
}
#region Открытые свойства - обертки закрытых полей
//------------------------------------------------------
//
// Открытые свойства - обертки закрытых полей
//
//------------------------------------------------------
// Содержимое текстового поля _findWhat
public string FindWhat
{
get
{
return _findWhat.Text;
}
set
{
_findWhat.Text = value;
}
}
// Содержимое текстового поля _replaceWith
public string ReplaceWith
{
get
{
return _replaceWith.Text;
}
set
{
_replaceWith.Text = value;
}
}
// Определение состояния флага учета регистра
public bool? MatchCase { get { return _matchCase.IsChecked; } }
// Определение состояния радиокнопки направления поиска назад
public bool? SearchUp { get { return _findUp.IsChecked; } }
// Управление видимостью интерфейса замены
// Hidden - элемент скрыт (не отображается, но занимает место)
// Collapsed - элемент свернут (не отображается и не занимает место)
public bool ShowReplace
{
get { return _replaceWith.Visibility == Visibility.Visible; }
set
{
Visibility show;
if (value)
{
// Отобразить
show = Visibility.Visible;
_directionGroupBox.Visibility = Visibility.Collapsed;
_findDown.IsChecked = true;
}
else
{
// Свернуть
show = Visibility.Collapsed;
_directionGroupBox.Visibility = Visibility.Visible;
}
_replaceLabel.Visibility = _replaceWith.Visibility =
_replace.Visibility = _replaceAll.Visibility = show;
}
}
#endregion Общедоступные свойства - обертки полей
#region Открытые события для обработки в основном классе
//------------------------------------------------------
//
// Открытые события для обработки в основном классе Window1
// Обеспечивают взаимодействие диалогового окна с владельцем
//
//------------------------------------------------------
// Объявляем немаршрутизованные события
public event EventHandler FindNext;
public event EventHandler Replace;
public event EventHandler ReplaceAll;
#endregion Открытые события для обработки в основном классе Window1
#region Закрытые обработчики
//------------------------------------------------------
//
// Закрытые методы
// При возбуждении событий первый параметр - ссылка на диалог,
// которую в основном классе приведем к самому диалоговому окну
//
//------------------------------------------------------
void OnActivated(object sender, EventArgs e)
{
_findWhat.Focus();
}
private void FindNextClicked(object sender, RoutedEventArgs e)
{
// Если на событие подписались, возбуждаем его
if (FindNext != null)
{
FindNext(this, EventArgs.Empty);
}
}
private void ReplaceClicked(object sender, RoutedEventArgs e)
{
// Возбуждаем событие, если для него существует обработчик
if (Replace != null)
{
Replace(this, EventArgs.Empty);
}
}
private void ReplaceAllClicked(object sender, RoutedEventArgs e)
{
// Проверяем наличие обработчика и возбуждаем событие
if (ReplaceAll != null)
{
ReplaceAll(this, EventArgs.Empty);
}
}
private void CancelClicked(object sender, RoutedEventArgs e)
{
this.Close();
}
private void FindTextChanged(object sender, TextChangedEventArgs e)
{
// Управление доступность кнопок в зависимости от текстового поля
_findNext.IsEnabled = _replace.IsEnabled = _replaceAll.IsEnabled
= !String.IsNullOrEmpty(_findWhat.Text);
}
#endregion Закрытые обработчики
}
}
При щелчке на кнопках собственные
обработчики кнопок возбуждают в диалоге события, которые будет прослушивать
основное окно, перехватывать их и обрабатывать своими обработчиками. При объявлении
свойств MatchCase и SearchUp использован тип " bool?", поскольку элементы CheckBox и RadioButton имеют три состояния: IsChecked=" False ", IsChecked=" True " и
IsChecked=" {x:Null} ".
Для выборки только булевых состояний при программном управлении в клиентском
коде этими элементами через открытые свойства нужно будет применять явное приведение
типов. Например, caseFlag=(bool)_dlg.MatchCase;, где _dlg - это экземпляр класса FindAndReplaceDialog в клиенте.
Запустите приложение
для проверки отсутствия синтаксических ошибокТеперь нужно подключить диалоговое окно Find and Replace к основному окну Window1 для совместной работы и наделить его соответствующей функциональностью.
Но прежде надо вновь назначить Window1 стартовым окном приложения.
Откройте файл App.xaml приложения Notepad1 и внесите изменения в открывающий дескриптор:Было StartupUri="FindAndReplaceDialog.xaml" Стало StartupUri="Window1.xaml"
Поскольку код программного управления диалоговым окном Find and Replace будет
несколько великоват, разместим его в отдельном файле с именем EditFind.cs.
В панели Solution
Explorer выделите узел проекта Notepad1 и вызовите командой меню Project/Add New Item одноименное
диалоговое окно оболочки, которое настройте так
>
После щелчка на кнопке Add мастер добавит пустой файл в текущий проект решения.
Заполните файл EditFind.cs следующим
кодомusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
internal FindAndReplaceDialog _dlg = null; // Для видимости в обработчиках
string _findString, _replaceString; // Для видимости в обработчиках
void CreateDialog()
{
if (_dlg != null)// Уже существует
{
// Если есть выделенное, обновляем прежнее
if (!String.IsNullOrEmpty(txtBox1.SelectedText))
_findString = _dlg._findWhat.SelectedText = txtBox1.SelectedText;
_dlg._findWhat.Focus();
return;
}
// Создать заново
_dlg = new FindAndReplaceDialog();
_dlg.Owner = this; // Привязываем диалог к владельцу
_dlg.Show();// Немодальное, поэтому не перехватывает управление
// Продолжаем настраивать
_dlg._findWhat.Focus();
// Если есть выделенное, обновляем прежнее
if (!String.IsNullOrEmpty(txtBox1.SelectedText))
_findString = _dlg._findWhat.SelectedText = txtBox1.SelectedText;
_dlg.ReplaceWith = _replaceString;
// Анонимные обработчики
_dlg.FindNext += delegate(object sender, EventArgs args)
{
FindNextExec();
}; //!!!
_dlg.Replace += delegate(object sender, EventArgs args)
{
ReplaceExec();
}; //!!!
_dlg.ReplaceAll += delegate(object sender, EventArgs args)
{ // У Петцольда (WPF с.466) есть иной вариант этого обработчика
_replaceString = _dlg.ReplaceWith;
txtBox1.SelectionStart = 0;
txtBox1.SelectionLength = 0;
while (FindNextExec())
{
using (txtBox1.DeclareChangeBlock())
{
txtBox1.SelectedText = _replaceString;
txtBox1.SelectionLength = _replaceString.Length;
}
}
txtBox1.SelectionStart = 0;
txtBox1.SelectionLength = 0;
}; //!!!
_dlg.Closed += delegate(object sender, EventArgs args)
{
_dlg = null;
}; //!!! Точка с запятой обязательна - заканчивает строку
}
bool FindNextExec()
{
int indexStart, indexFind;// Откуда начать и начало следующего
_findString = _dlg.FindWhat; // Извлекаем текст поиска
// Учет регистра при поиске, однострочный условный оператор
StringComparison strComp = (bool)_dlg.MatchCase ? StringComparison.Ordinal :
StringComparison.OrdinalIgnoreCase;
if ((bool)_dlg.SearchUp)// Ищем вверх
{
indexStart = txtBox1.SelectionStart - 1;
indexFind = txtBox1.Text.LastIndexOf(_findString, indexStart, strComp);
}
else // Ищем вниз
{
indexStart = txtBox1.SelectionStart + txtBox1.SelectionLength;
indexFind = txtBox1.Text.IndexOf(_findString, indexStart, strComp);
}
// Анализируем и принимаем решение
if (indexFind != -1)
{
txtBox1.Select(indexFind, _findString.Length);// Выделяем найденное
txtBox1.Focus();
return true;
}
else
{
MessageBox.Show("Текст \"" + _findString + "\" не найден!",
this.Title, MessageBoxButton.OK, MessageBoxImage.Information);
txtBox1.Focus();
return false;
}
}
private void ReplaceExec()
{
// Извлекаем тексты поиска и замены
_findString = _dlg.FindWhat;
_replaceString = _dlg.ReplaceWith;
// Учет регистра при поиске, однострочный условный оператор
StringComparison strComp = (bool)_dlg.MatchCase ? StringComparison.Ordinal :
StringComparison.OrdinalIgnoreCase;
if (_findString.Equals(txtBox1.SelectedText, strComp))
txtBox1.SelectedText = _replaceString;
FindNextExec();
}
}
}
Привязать к владельцу диалоговое окно нужно раньше, чем оно будет нарисовано, иначе заданная опция центрирования относительно владельца действовать не будет!
Код получился (на мой
взгляд!) интересным и поучительным - попробуйте разобраться
в немДля разнообразия, чтобы не придумывать новые имена обработчикам, которые
все равно как методы нигде далее сами вызывать не будем (кроме как автоматически
через события), мы применили синтаксис анонимных обработчиков.
Тот код, который нам придется вызывать не только через события диалогового окна,
но и через интерфейсные элементы основного окна Window1, упакован в отдельные
функции.
Обратите внимание, что при объявлении ссылки на экземпляр окна мы применили
модификатор доступности internal (внутренний).
Этот оператор обычно используется для типов (а не членов
типа), видимость которых нужно ограничить только текущей сборкой. В
нашем случае это ни на что не влияет и его можно убрать или заменить на private,
но интересно отметить вот что...
Откройте панель Class View в разделе View оболочки и посмотрите на пиктограммы членов класса Window1,
который мы как раз сейчас и расширяем
Оказывается, что те члены-поля класса, которые мы применили как элементы в
разметке, считаются с видимостью internal (конверт на пиктограмме). А все члены
класса, которые мы объявили в процедурном коде без указания
Теперь нам осталась самая малость - подключить готовый код функциональности Find and Replace к обработчикам
интерфейсных элементов в основном окне и испытать его работу. Заготовки обработчиков
мы создавали ранее и они находятся в файле Edit.cs.
Добавьте в соответствующие
обработчики файла Edit.cs следующий кодprivate void FindOnExecute(object sender, RoutedEventArgs e)
{
CreateDialog();
}
private void FindNextOnExecute(object sender, RoutedEventArgs e)
{
if (_dlg == null)
CreateDialog();
else
FindNextExec();
}
private void ReplaceOnExecute(object sender, RoutedEventArgs e)
{
if (_dlg == null)
{
CreateDialog();
_dlg.ShowReplace = true;
}
else if (_dlg.ShowReplace == false)
_dlg.ShowReplace = true;
else
ReplaceExec();
}
private void GoToOnExecute(object sender, RoutedEventArgs e)
{
}
Запустите проект Notepad1 и испытайте функциональность Find
and Replace. Разберитесь с кодомНачальный интерфейс диалогового окна для задачи Replace будет таким

Когда диалоговое окно Find and Replace создано, оно размещается поверх своего
родителя. Кнопки диалогового окна возбуждают события, которые обрабатываются
кодом основного окна, содержащего текст. Тот же самый код может вызываться
и в обработчиках основного окна, если пользователь управляет через интерфейсные
элементы основного окна. Этот подход чем-то напоминает автомобиль с дублирующими
педалями.
На панели инструментов осталось незадействованным текстовое поле поиска, обозначенное в разметке как
<ToolBar Header="Find:">
<TextBox Width="100" />
<Button Name="btnFind" Click="FindOnExecute" Width="23" Content="{StaticResource iconFind}" />
</ToolBar>
Я оставил его в покое (надоело), попробуйте сами, если хотите, использовать
его в коде, но для этого прежде всего ему нужно присвоить имя, например, txtFind.
Ну а лучше - пусть болтается для солидности.
Следующим шагом реализуем последнюю задачу раздела Edit в меню нашего приложения
- Go To... Как видно из запланированного (опрометчиво) нами многоточия
после названия задачи, это будет опять связано с разработкой диалогового окна.
Причем, это окно должно быть модальным, потому что выполняет одноразовую задачу.
В панели Solution
Explorer выделите узел проекта Notepad1 и добавьте командой Project/Add Window
новое окно WPF с именем GoToDialog.xaml
>
Заполните файл GoToDialog.xaml следующей
разметкой<Window x:Class="Notepad1.GoToDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
SizeToContent="WidthAndHeight"
WindowStartupLocation="CenterOwner"
ShowInTaskbar="false"
Title="Go To Line"
ResizeMode="NoResize"
Activated="OnActivated"
>
<StackPanel
Background="{StaticResource DialogBackgroundBrush}"
>
<StackPanel
Orientation="Vertical"
Margin="5"
>
<Label
Name="_rangeNumber"
Margin="0 0 8 3"
VerticalAlignment="Bottom"
Content="_Line number" />
<TextBox
Name="_lineNumberTextBox"
TabIndex="0"
MinWidth="220"
Margin="5,0" />
</StackPanel>
<StackPanel
Orientation="Horizontal"
Margin="5,0,5,5" HorizontalAlignment="Right">
<Button
Click="OkClicked"
IsDefault="true"
MinWidth="75"
MinHeight="23"
Content="OK" />
<Button
Click="CancelClicked"
IsCancel="true"
Margin="15 0 5 0"
MinWidth="75"
MinHeight="23"
Content="Cancel" />
</StackPanel>
</StackPanel>
</Window>
Представление окна в графическом конструкторе по данной разметке будет таким

Обратите внимание, что для кнопки OK свойство IsDefault="true",
чтобы пользователь мог закрыть окно клавишей Enter, а для второй кнопки IsCancel=true - чтобы выйти по клавише Esc.
Пройдитесь по разметке
и командой Navigate to Event Handler контекстного меню для
записей событий создайте заготовки обработчиков в файле процедурного кода GoToDialog.xaml.csДля тренировки воспользуемся способом хранения и извлечения некоторых сообщений в ресурсах сборки.
В панели Solution
Explorer вызовите контекстное меню для узла проекта и добавьте командой Add/New Item текстовый файл с любым именем (оставьте по умолчанию)
>
В панели Solution
Explorer вызовите контекстное меню на созданном текстовом файле и командой Rename присвойте ему новое имя StringTable.ru-ru.resTextПервая часть имени может быть произвольной, а два остальных расширения нужно строго соблюсти. Одно означает культуру, а второе служебное для менеджера ресурсов.
В панели Solution
Explorer выделите файл StringTable.ru-ru.resText и в панели Properties установите
для него следующие директивы:
Заполните файл StringTable.ru-ru.resText следующими парами ключ=значениеGotoErrorDialogTitle=Go To Line Error GotoErrorMsgFormat=Текстовое поле должно\nсодержать целое число GotoErrorMsgEmpty=Введите число в текстовое поле GotoErrorMsgRange=Выход за границы диапазона
Заполните файл GoToDialog.xaml.cs следующим процедурным кодомusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
// Дополнительные подключения пространств имен
using System.Resources;
using System.Reflection;
namespace Notepad1
{
public partial class GoToDialog : Window
{
public GoToDialog()
{
InitializeComponent();
// Сохраняем первое значение Label
_rangeOrigin = _rangeNumber.Content.ToString();
}
// Объявляем событие для прослушивания в основном окне
public event EventHandler GotoActivate;
// Закрытые поля класса
int _lineNumber = 0;
int _maxLineNumber;
ResourceManager res =
new ResourceManager("Notepad1.StringTable",
Assembly.GetExecutingAssembly());
String _rangeOrigin;
// Свойства доступа
public int LineNumber
{
get { return _lineNumber; }
set
{
_lineNumber = value;
_lineNumberTextBox.Text = _lineNumber.ToString();
}
}
public int MaxLineNumber
{
get { return _maxLineNumber; }
set { _maxLineNumber = value; }
}
private void OnActivated(object sender, EventArgs e)
{
// Если существуют обработчики, инициируем событие
if (GotoActivate != null)
GotoActivate(this, EventArgs.Empty);
_rangeNumber.Content = _rangeOrigin +
String.Format(" (1 - {0}):", _maxLineNumber);
_lineNumberTextBox.Focus();
_lineNumberTextBox.SelectAll();// Выделяем содержимое
}
private void OkClicked(object sender, RoutedEventArgs e)
{
// Не закрывать диалог, пока есть ошибки
if (string.IsNullOrEmpty(_lineNumberTextBox.Text))
{
MessageBox.Show(res.GetString("GotoErrorMsgEmpty"),
res.GetString("GotoErrorDialogTitle"));
return;
}
if (!int.TryParse(_lineNumberTextBox.Text, out _lineNumber))
{
MessageBox.Show(res.GetString("GotoErrorMsgFormat"),
res.GetString("GotoErrorDialogTitle"));
return;
}
if (LineNumber > _maxLineNumber || LineNumber <= 0)
{
MessageBox.Show(res.GetString("GotoErrorMsgRange"),
res.GetString("GotoErrorDialogTitle"));
return;
}
this.DialogResult = true;
this.Close();
}
private void CancelClicked(object sender, RoutedEventArgs e)
{
this.DialogResult = false;
this.Close();
}
}
}
Обратите внимание на то, как мы в коде извлекаем ресурсы, вложенные (Embedded)
ранее в текстовый файл StringTable.ru-ru.resText.
Подключите диалоговое
окно Go To к обработчику главного окна в файле Edit.cs (для удобства
код файла приводится полностью)using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Обработчики источников задач Edit
//
//------------------------------------------------------
private void UndoOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Undo();
}
private void RedoOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Redo();
}
private void CutOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Cut();
// Вариант
//Clipboard.SetText(txtBox1.SelectedText);
//txtBox1.SelectedText = "";
}
private void CopyOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Copy();
// Вариант
//Clipboard.SetText(txtBox1.SelectedText);
}
private void PasteOnExecute(object sender, RoutedEventArgs e)
{
// Если в буфере содержатся данные текстового формата
if (Clipboard.ContainsText())
txtBox1.Paste();
}
private void DeleteOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.SelectedText = String.Empty;
}
private void FindOnExecute(object sender, RoutedEventArgs e)
{
CreateDialog();
}
private void FindNextOnExecute(object sender, RoutedEventArgs e)
{
if (_dlg == null)
CreateDialog();
else
FindNextExec();
}
private void ReplaceOnExecute(object sender, RoutedEventArgs e)
{
if (_dlg == null)
{
CreateDialog();
_dlg.ShowReplace = true;
}
else if (_dlg.ShowReplace == false)
_dlg.ShowReplace = true;
else
ReplaceExec();
}
#region Задача Go To
private void GoToOnExecute(object sender, RoutedEventArgs e)
{
GoToDialog dlg = new GoToDialog();
dlg.Owner = this;
dlg.LineNumber = CaretLineNumber;
/***** Врет для завернутых строк Word Wrap! ******************
dlg.MaxLineNumber = txtBox1.LineCount;
/*************************************************************/
dlg.MaxLineNumber = GetMaxNumber();
// Параметры анонимного обработчика "от фонаря",
// все равно не используем
dlg.GotoActivate += delegate(object sender2, EventArgs e2)
{
// Заполняем текстовое поле диалога текущей строкой
dlg.LineNumber = CaretLineNumber;
};
// Запускаем в модальном режиме. Ждет и закрывается
if (dlg.ShowDialog() == true)
{
// Устанавливаем каретку (курсор) в начало строки
CaretLineNumber = dlg.LineNumber;
}
txtBox1.Focus();
}
int GetMaxNumber()
{
int count = 0;
int pos = 0;
int caretPos = txtBox1.Text.Length + 1;
while (pos < caretPos)
{
count++; // Счетчик строк
pos = txtBox1.Text.IndexOf("\r\n", pos);
if (pos != -1) // Нашли очередную пару
pos += 2; // Сдвигаемся правее найденных
else
break;// Больше нет
}
return count;
}
int CaretLineNumber
{
get
{
/***** Врет для завернутых строк Word Wrap! ******************
int caretPos = txtBox1.SelectionStart;
return (txtBox1.GetLineIndexFromCharacterIndex(caretPos) + 1);
/*************************************************************/
int count = 0;
int pos = 0;
int caretPos = txtBox1.SelectionStart + 1; //txtBox1.CaretIndex
while (pos < caretPos)
{
count++; // Счетчик строк
pos = txtBox1.Text.IndexOf("\r\n", pos);// \n - перевод строки
if (pos != -1) // Нашли очередную пару
pos += 2; // Сдвигаемся правее найденных
else
break;// Больше нет
}
return count;
}
set
{
value = value - 1;
int count = 0;
int pos = 0;
while (count < value) // Пока не превышает заданную
{
pos = txtBox1.Text.IndexOf("\r\n", pos);// \r - возврат каретки
if (pos != -1) // Нашли очередную пару
{
count++; // Счетчик строк
pos += 2; // Сдвигаемся правее найденных
}
else
{
pos = txtBox1.Text.Length;
break;
}
}
// Позиционируем курсор
txtBox1.SelectionStart = pos;
txtBox1.SelectionLength = 0;
}
}
#endregion Задача Go To
private void SelectAllOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.SelectAll();
}
}
}
Запустите проект и
испытайте введенную функциональность Go To. Разберитесь с кодомВ нашем приложении остались еще три запланированные задачи, обработчики которых
мы разместили в файле Other.cs. Задачи Font и About требуют создания диалоговых
окон. Чтобы не усложнять себе жизнь и не отвлекаться, ограничимся только
простым показом стандартных диалогов.
Заполните обработчики
в файле Other.cs следующим кодомusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Прочие обработчики
//
//------------------------------------------------------
private void FontOnExecute(object sender, RoutedEventArgs e)
{
System.Windows.Forms.FontDialog dlg =
new System.Windows.Forms.FontDialog();
switch (dlg.ShowDialog())
{
case System.Windows.Forms.DialogResult.Abort:
case System.Windows.Forms.DialogResult.Cancel:
case System.Windows.Forms.DialogResult.Ignore:
case System.Windows.Forms.DialogResult.No:
case System.Windows.Forms.DialogResult.None:
case System.Windows.Forms.DialogResult.OK:
case System.Windows.Forms.DialogResult.Retry:
case System.Windows.Forms.DialogResult.Yes:
break;
}
}
private void WordWrapOnExecute(object sender, RoutedEventArgs e)
{
if (itemWordWrap.IsChecked)
txtBox1.TextWrapping = TextWrapping.Wrap;
else
txtBox1.TextWrapping = TextWrapping.NoWrap;
}
private void AboutOnExecute(object sender, RoutedEventArgs e)
{
MessageBox.Show("Сведения об авторах...\n" +
"Сведения о программе...", "About");
}
}
}
Запустите проект и
проверьте добавленную функциональностьОбратите внимание, что когда заворот строк выключен, то правая часть широкой строки уходит за рамки текстового элемента и становится невидимой. Текстовый элемент поля редактирования по умолчанию настроен так, что вертикальная скролирующая линейка появляется автоматически, когда текст не вмещается по вертикали. Но горизонтальная полоса скролирования по умолчанию отключена. Исправим это.
Добавьте в разметку
открывающего дескриптора элемента txtBox1 в файле Window1.xaml следующий
атрибут<!-- Многострочное текстовое поле редактирования -->
<TextBox TextWrapping="Wrap"
AcceptsReturn="True"
AcceptsTab="True"
VerticalScrollBarVisibility="Auto"
Name="txtBox1"
TextChanged="txtBox1_TextChanged"
HorizontalScrollBarVisibility="Auto"
>
Запустите проект и
проверьте добавленную функциональностьЕсли внимательно посмотреть на работающее приложение, то можно заметить один
неприятный нюанс. Когда по щелчку мыши вызываются диалоговые окна и они перекрывают
элементы меню или кнопки, то после закрытия диалогов интерфейсные элементы
не перерисовываются. Это происходит потому, что элементы теряют фокус за
счет переключения на другое окно и мышь уходит (leave) с них не по установленной
схеме. Как заставить окно WPF перерисоваться (paint), я так и не нашел (пока),
поэтому ничего не оставалось, как применить следующий 'убогий' способ...
В файле Window1.xaml добавьте в открывающий дескриптор окна подписку на событие Activated<Window x:Class="Notepad1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
Closing="Window_Closing"
Activated="Window_Activated"
>
.........................................
</Window>
Создайте обработчик Window_Activated() в файле Window1.xaml
.cs и заполните его такprivate void Window_Activated(object sender, EventArgs e)
{
this.Height = this.ActualHeight - 1;
this.Height = this.ActualHeight + 1;
this.txtBox1.Focus();
}
Испытайте приложение
- окно снизу немного дергается, но возможно пользователь этого не заметит
(кто сейчас смотрит под ноги!), зато интерфейсные элементы "не залипают"Когда мы создавали источники - элементы меню, то бодро прописали вместе с названиями задач и жесты клавиатурного ввода. Но ни один из них в настоящий момент не работает, поскольку не подкреплен соответствующим механизмом. Далее мы этот же пример повторим с применением команд, а там, мы говорили, эта функциональность уже встроена. Но на данном этапе нам такую возможность придется реализовывать вручную, и мы это сейчас выполним - для тренировки!
Добавьте к узлу текущего
проекта пустой файл KeyGestures.cs
>
Заполните файл KeyGestures.cs следующим кодомusing System;
using System.Windows;
using System.Windows.Input;
using System.Collections.Generic;// Для Dictionary<TKey, TValue>
namespace Notepad1
{
partial class Window1
{ // Еще один вариант в Петцольд, WPF, с.316 !!!
// Определяем ассоциативный словарь
Dictionary<KeyGesture, RoutedEventHandler> gests =
new Dictionary<KeyGesture, RoutedEventHandler>();
void CreateGestures()
{
// File
gests.Add(new KeyGesture(Key.N, ModifierKeys.Control), NewOnExecute);//_New
gests.Add(new KeyGesture(Key.O, ModifierKeys.Control), OpenOnExecute);//_Open...
gests.Add(new KeyGesture(Key.S, ModifierKeys.Control), SaveOnExecute);//_Save
gests.Add(new KeyGesture(Key.F2, ModifierKeys.Control), PrintPreviewOnExecute);//P_rint Preview
gests.Add(new KeyGesture(Key.P, ModifierKeys.Control), PrintOnExecute);//_Print...
// Edit
gests.Add(new KeyGesture(Key.Z, ModifierKeys.Control), UndoOnExecute);//_Undo
gests.Add(new KeyGesture(Key.Y, ModifierKeys.Control), RedoOnExecute);//_Redo
gests.Add(new KeyGesture(Key.X, ModifierKeys.Control), CutOnExecute);//Cu_t
gests.Add(new KeyGesture(Key.C, ModifierKeys.Control), CopyOnExecute);//_Copy
gests.Add(new KeyGesture(Key.V, ModifierKeys.Control), PasteOnExecute);//_Paste
gests.Add(new KeyGesture(Key.Delete, ModifierKeys.None), DeleteOnExecute);//De_lete
gests.Add(new KeyGesture(Key.F, ModifierKeys.Control), FindOnExecute);//_Find...
gests.Add(new KeyGesture(Key.F3, ModifierKeys.None), FindNextOnExecute);//Find _Next
gests.Add(new KeyGesture(Key.H, ModifierKeys.Control), ReplaceOnExecute);//_Replace...
gests.Add(new KeyGesture(Key.G, ModifierKeys.Control), GoToOnExecute);//_Go To...
gests.Add(new KeyGesture(Key.A, ModifierKeys.Control), SelectAllOnExecute);//Select _All
// Format
gests.Add(new KeyGesture(Key.W, ModifierKeys.Control), WordWrapOnExecute);//_Word Wrap
}
// Перекрываем стандартный обработчик
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
base.OnPreviewKeyDown(e);
// Ищем жест, останавливаем событие и исполняем обработчик
foreach (KeyGesture gest in gests.Keys)
if (gest.Matches(null, e)) // Сравниваем перехваченный жест с заданным в объекте
{
gests[gest](this, e); // Вызываем обработчик через словарь
e.Handled = true; // Останавливаем событие
break; // Прерываем цикл
}
}
}
}
Добавьте в конструктор
класса Window1 файла Window1.xaml.cs вызов нашей функции создания жестовpublic Window1()
{
InitializeComponent();
// Создание жестов
this.CreateGestures();
}
Запустите приложение
и убедитесь, что клавиатурные жесты работаютОдной из важных задач управления пользовательским интерфейсом является своевременное отключение источников команд, когда выполнение команды может противоречить логике работы приложения. Например, если в редакторе текста буфер обмена пуст, команду вставки следует сделать недоступной, или, если в загруженном файле не было изменений, то кнопку сохранения тоже нужно отключить.
Для нашего приложения проведем ревизию правил, по которым элементы интерфейса должны менять свое состояние в зависимости от возможности выполнения тех или иных задач на текущий момент.
Раздел File:
New: должна быть доступна всегда, поскольку пользователь
может захотеть в любой момент создать новый документ. Если в текущем документе
есть несохраненные изменения, то нужно вывести диалоговое окно с предложением
их сохранить, проигнорировать или отменить задачу. При утвердительном ответе
пользователя следует проверить, если новый документ сохраняется впервые,
то нужно предоставить диалоговое окно записи. Реализована!Open: аналогична задаче New, только после решения о сохранении
текущих изменений следует предоставить диалог открытия файла. Реализована!Save: при новом документе предоставить диалог записи. Если
документ уже сохранялся и имеет имя, но текущих изменений нет, то задачу
следует сделать недоступной, отключив соответствующие источники. При первом
же изменении нужно немедленно освободить источники выполнения этой задачи. Нереализована!Save As: должна быть доступна всегда. При запросе выполнения
этой задачи сразу предоставить пользователю диалог сохранения файла. Реализована!Page Setup, Print Preview, Print:
должны быть доступны всегдаExit: должна быть доступна всегда. Если есть несохраненные
изменения, частично выполнить задачу New и завершить приложение (возможно,
с сохранением текущего состояния приложения в ресурсном файле). Реализована
частично!Раздел Edit:
Undo, Redo: делать недоступными, когда восстанавливать
нечего. Нереализована!Cut, Copy: делать недоступными, если нет выделения
текста. Нереализована!Paste: делать недоступной, если буфер обмена пуст или в
нем сохранена нетекстовая информация. Нереализована!Delete: делать недоступной, если нет выделения текста. Нереализована!Find, Find Next, Replace, Go To:
доступны всегдаSelect All: недоступна в случае, если текстовое поле пустое. Нереализована!Раздел Format:
Font, Word Wrap: доступны всегдаРаздел Help:
About: доступна всегдаЧтобы продемонстрировать трудность реализации логики отключения источников,
ограничимся только одной задачей Save. В последующем упражнении, где будет
использован механизм команд, все решится гораздо проще. А пока только одна
задача - Save, чтобы зря не тратить силы.
Не хочется вмешиваться в ранее разработанный код, поскольку логику отключения мы оставили на потом и сейчас это может повлечь ошибки. Поэтому, наиболее разумно, добавить автономный код, не меняя прежнего, и разместить его в отдельном файле.
Выделите узел текущего
проекта и командой Project/Add Class добавьте новый файл с именем EnabledControls.cs,
который заполните такusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Controls.Primitives;// Для ButtonBase
namespace Notepad1
{
partial class Window1
{
}
}
Первое, что приходит
на ум - использовать событие texBox1.TextChanged, в котором проверять состояние
флага IsModified и принимать решение о недоступности или доступности источников
задачи Save.
Добавьте в файл EnabledControls.cs следующий
код, регистрирующий еще один обработчик события texBox1.TextChangednamespace Notepad1
{
partial class Window1
{
// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
// Еще один обработчик
// обычного события TextChanged
txtBox1.TextChanged += EnabledControls_TextChanged;
}
private void EnabledControls_TextChanged(object sender,
System.Windows.Controls.TextChangedEventArgs e)
{
// Изменяем состояние интерфейсных элементов _Save
itemSave.IsEnabled = btnSave.IsEnabled = IsModified;
}
}
}
В файле Window1.xaml.cs добавьте в конструктор класса Window1 последней строкой вызов функции AdditionalHandlers() такpublic Window1()
{
InitializeComponent();
// Создание жестов
this.CreateGestures();
// Дополнительные обработчики в файле EnabledControls.cs
AdditionalHandlers();
}
Запустите приложение
- до первого изменения текста кнопки источники Save блокированы, а потом
все работает не так. И жест Ctrl+S тоже доступен.Дело здесь в том, что событие TextChanged срабатывает раньше, чем будет установлен
флаг IsModified. Поэтому нужно обрабатывать не событие изменения текста, а
событие изменения флага IsModified. Следующим шагом мы преобразуем поле IsModified в свойство на базе нового логического поля modified и создадим свое событие,
в обработчике которого и решим управление доступностью источников задачи Save.
В файле Window1.xaml.cs найдите объявление поля IsModified и переименуйте его в modifiedБыло bool IsModified = false; // Флаг изменений содержимого Стало bool modified = false; // Флаг изменений содержимого
Поле modified будет базовым для свойства IsModified. Это все изменения, которые
мы вынуждены были провести в прежнем коде. Остальные изменения будем вносить
в файл EnabledControls.cs.
В файле EnabledControls.cs удалите весь код, связанный с событием texBox1.TextChanged и его обработчикомnamespace Notepad1
{
partial class Window1
{
// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
// Еще один обработчик
// обычного события TextChanged
txtBox1.TextChanged += EnabledControls_TextChanged;
}
private void EnabledControls_TextChanged(object sender,
System.Windows.Controls.TextChangedEventArgs e)
{
// Изменяем состояние интерфейсных элементов _Save
itemSave.IsEnabled = btnSave.IsEnabled = IsModified;
}
}
}
Добавьте в файл EnabledControls.cs новый код, чтобы файл стал такимusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Controls.Primitives;// Для ButtonBase
namespace Notepad1
{
partial class Window1
{
// Объявляем внутреннее событие
private event EventHandler ChangeModifiedEvent;
// Упаковываем базовое поле modified в свойство
private bool IsModified
{
get { return modified; }
set
{
if (modified != value)
{
modified = !modified;
// Инициируем событие, если есть обработчик
if (ChangeModifiedEvent != null)
ChangeModifiedEvent(this, EventArgs.Empty);
}
}
}
// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
// Начальные запрещения для _Save
itemSave.IsEnabled = btnSave.IsEnabled = false;
// Удаляем созданный в CreateGestures() жест _Save
foreach (KeyGesture gest in gests.Keys)
if (gests[gest] == SaveOnExecute)
{
gests.Remove(gest);
break;
}
// Регистрируем обработчик изменения свойства
this.ChangeModifiedEvent += Window1_ChangeModifiedEvent;
}
void Window1_ChangeModifiedEvent(object sender, EventArgs e)
{
//MessageBox.Show("Modify");
// Проверяем состояние любого из источников _Save
if (btnSave.IsEnabled == false)
// Добавляем жест _Save
gests.Add(new KeyGesture(Key.S, ModifierKeys.Control),
SaveOnExecute);//_Save
else
// Удаляем жест _Save
foreach (KeyGesture gest in gests.Keys)
if (gests[gest] == SaveOnExecute)
{
gests.Remove(gest);
break;
}
// Изменяем состояние интерфейсных элементов _Save
itemSave.IsEnabled = btnSave.IsEnabled = IsModified;
}
}
}
Запустите приложение
и убедитесь, что управление источниками задачи Save, включая жесты, во всех
режимах работает как и положено. Разберитесь с кодом!!!Для управления доступностью других задач приложения нужно построить что-то
подобное. Мы этого здесь делать не будем, однако и сейчас уже ясно, что это
непростая задача. Для желающих продолжить управление отключениями источников
можно посоветовать дополнить файл EnabledControls.cs новыми заготовками так
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Controls.Primitives;// Для ButtonBase
namespace Notepad1
{
partial class Window1
{
// Объявляем внутреннее событие
private event EventHandler ChangeModifiedEvent;
// Упаковываем базовое поле modified в свойство
private bool IsModified
{
get { return modified; }
set
{
if (modified != value)
{
modified = !modified;
// Инициируем событие, если есть обработчик
if (ChangeModifiedEvent != null)
ChangeModifiedEvent(this, EventArgs.Empty);
}
}
}
// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
// Регистрируем один и тот же обработчик
// всплывающих событий кнопок, элементов меню,
// клавиатурных жестов для окна
this.AddHandler(ButtonBase.ClickEvent,
new RoutedEventHandler(this.Window1_ButtonClick));
this.AddHandler(MenuItem.ClickEvent,
new RoutedEventHandler(this.Window1_ItemClick));
this.AddHandler(Keyboard.KeyDownEvent,
new RoutedEventHandler(this.Window1_Gesture));
// Дополнительный общий обработчик элементов контекстного меню
contextCut.Click += new RoutedEventHandler(item_Context);
contextCopy.Click += new RoutedEventHandler(item_Context);
contextPaste.Click += item_Context; // Упрощенный синтаксис
contextDelete.Click += item_Context;
// Начальные запрещения для _Save
itemSave.IsEnabled = btnSave.IsEnabled = false;
// Удаляем созданный в CreateGestures() жест _Save
foreach (KeyGesture gest in gests.Keys)
if (gests[gest] == SaveOnExecute)
{
gests.Remove(gest);
break;
}
// Регистрируем обработчик изменения свойства
this.ChangeModifiedEvent += Window1_ChangeModifiedEvent;
}
void Window1_ChangeModifiedEvent(object sender, EventArgs e)
{
//MessageBox.Show("Modify");
// Проверяем состояние любого из источников _Save
if (btnSave.IsEnabled == false)
// Добавляем жест _Save
gests.Add(new KeyGesture(Key.S, ModifierKeys.Control),
SaveOnExecute);//_Save
else
// Удаляем жест _Save
foreach (KeyGesture gest in gests.Keys)
if (gests[gest] == SaveOnExecute)
{
gests.Remove(gest);
break;
}
// Изменяем состояние интерфейсных элементов _Save
itemSave.IsEnabled = btnSave.IsEnabled = IsModified;
}
private void Window1_ButtonClick(object sender, RoutedEventArgs e)
{
//MessageBox.Show("Button");
// Повышаем полномочия ссылки
Button btn = sender as Button;
if (btn == btnSave)
{
;
}
}
private void Window1_ItemClick(object sender, RoutedEventArgs e)
{
//MessageBox.Show("Item");
// Повышаем полномочия ссылки
MenuItem item = sender as MenuItem;
if (item == itemSave)
{
;
}
}
private void Window1_Gesture(object sender, RoutedEventArgs e)
{
//MessageBox.Show("Key");
}
private void item_Context(object sender, RoutedEventArgs e)
{
//MessageBox.Show("Context");
}
}
}
Добавленный код пока ни на что не влияет, но может стать отправной точкой
для дальнейших действий по блокированию других задач. В следующим упражнении
мы все решим гораздо проще, используя встроенный в WPF механизм команд.
Мы уже столько потрудились над этим блокнотом, который так наивно назвали простым, что неразумно будет начинать все заново. Проще скопировать полученный проект в новый и там проводить все необходимые изменения. Но трудились мы не для сего блокнота, а ради будущих наших профессиональных успехов (Se La Vi - такова жизнь).
Зайдите в проводнике Windows Explorer в каталог решения EventsAndCommands и скопируйте папку Notepad1
Переименуйте копию
проекта в Notepad2
Зайдите внутрь папки Notepad2 и переименуйте файл проекта тоже в Notepad2. Здесь
же удалите каталоги obj и bin, которые оболочка вновь создаст при запуске приложения
Запустите оболочку,
выделите EventsAndCommands и командой File/Add/Existing Project
зарегистрируйте в решении оболочки проект Notepad2
Не запуская проект,
откройте на редактирование любой файл проекта Notepad2, вызовите жестом Ctrl+H окно замены с указанными на рисунке настройками и сделайте замену по
всему проекту
Должно получиться 20 замен.
В панели Solution
Explorer вызовите контекстное меню для узла проекта Notepad2 и командой Set as StartUp Project назначьте его стартовым
Запустите приложение Notepad2 и убедитесь, что все работает как в проекте Notepad1Теперь будем модифицировать этот проект, подгоняя его под использование механизма
команд. Но прежде проведем ревизию библиотечных команд, возможно не все из
них закрывают наши источники и часть команд придется создавать самим. За
основу возьмем источники меню, поскольку там наиболее полно представлены
задачи нашего приложения. Интересующие нас библиотечные команды находятся
в классе ApplicationCommands пространства имен System.Windows.Input
| Раздел меню | Задача | Библиотечная команда | Встроенные жесты | Наши жесты | Регулировать доступность ? |
|---|---|---|---|---|---|
| File | New | ApplicationCommands.New | Ctrl+N | Ctrl+N | |
| Open | ApplicationCommands.Open | Ctrl+O | Ctrl+O | ||
| Save | ApplicationCommands.Save | Ctrl+S | Ctrl+S | Да | |
| Save As | ApplicationCommands.SaveAs | ||||
| Page Setup | |||||
| Print Preview | ApplicationCommands.PrintPreview | Ctrl+F2 | Ctrl+F2 | ||
| ApplicationCommands.Print | Ctrl+P | Ctrl+P | |||
| Exit | |||||
| Edit | Undo | ApplicationCommands.Undo | Ctrl+Z | Ctrl+Z | Да |
| Redo | ApplicationCommands.Redo | Ctrl+Y | Ctrl+Y | Да | |
| Cut | ApplicationCommands.Cut | Ctrl+X | Ctrl+X | Да | |
| Copy | ApplicationCommands.Copy | Ctrl+C | Ctrl+C | Да | |
| Paste | ApplicationCommands.Paste | Ctrl+V | Ctrl+V | Да | |
| Delete | ApplicationCommands.Delete | Del | Del | Да | |
| Find | ApplicationCommands.Find | Ctrl+F | Ctrl+F | ||
| Find Next | F3 | ||||
| Replace | ApplicationCommands.Replace | Ctrl+H | Ctrl+H | Да | |
| Go To | Ctrl+G | ||||
| Select All | ApplicationCommands.SelectAll | Ctrl+A | Ctrl+A | Да | |
| Format | Font | ||||
| Word Wrap | Ctrl+W | ||||
| Help | About |
Глядя на таблицу, можно сделать следующие выводы для нашего приложения:
Недостающие команды нужно создавать в процедурном коде. А вот подключать
все команды к источникам и связывать с коллекцией CommandBindings окна, пять
же для тренировки, будем и в разметке, и в процедурном коде.
Еще раз выпишем те задачи, которые мы будем реализовывать с помощью команд
| Где присоединить к источнику? | Где привязать к окну? | Задача | Библиотечная команда | Alias (псевдоним) | Жесты | Рег? |
|---|---|---|---|---|---|---|
| Разметка | Разметка | Save | Save | SaveCommand | Ctrl+S | Да |
| Разметка | Разметка | Page Setup | PageSetupCommand | Нет | ||
| Разметка | Разметка | Undo | Undo | UndoCommand | Ctrl+Z | Да |
| Разметка | Код | Redo | Redo | RedoCommand | Ctrl+Y | Да |
| Разметка | Код | Cut | Cut | CutCommand | Ctrl+X | Да |
| Разметка | Код | Copy | Copy | CopyCommand | Ctrl+C | Да |
| Код | Разметка | Paste | Paste | PasteCommand | Ctrl+V | Да |
| Код | Разметка | Delete | Delete | DeleteCommand | Del | Да |
| Код | Разметка | Find Next | FindNextCommand | F3 |
Да | |
| Код | Код | Replace | Replace | ReplaceCommand | Ctrl+H | Да |
| Код | Код | Go To | GoToCommand | Ctrl+G |
Нет | |
| Код | Код | Select All | SelectAll | SelectAllCommand | Ctrl+A | Да |
Мы выбрали 12 задач, которые хотим реализовать с помощью команд. Эта табличка будет нашим планом для дальнейшей работы.
Откройте файл Window1.xaml.cs текущего
проекта Notepad2, найдите объявление поля modified и переименуйте
его вновь в IsModified такБыло bool modified = false; // Флаг изменений содержимого Стало bool IsModified = false; // Флаг изменений содержимого
Откройте файл EnabledControls.cs,
уберите в нем лишний код, чтобы остался только такойusing System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad2
{
partial class Window1
{
// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
}
}
}
Внесите в файл EnabledControls.cs код определения и инициализации команд вместе со встроенными
жестамиnamespace Notepad2
{
partial class Window1
{
// Объявляем и инициализируем поля команд
// Их обязательно нужно объявлять статическими, чтобы
// размещались в объекте-типе и уже были созданы перед
// созданием элементов, к которым присоединяются
// Последний параметр означает жесты
public static RoutedCommand SaveCommand = ApplicationCommands.Save;
public static RoutedCommand PageSetupCommand =
new RoutedCommand("PageSetup", typeof(Window1), null);// Без жеста
public static RoutedCommand UndoCommand = ApplicationCommands.Undo;
public static RoutedCommand RedoCommand = ApplicationCommands.Redo;
public static RoutedCommand CutCommand = ApplicationCommands.Cut;
public static RoutedCommand CopyCommand = ApplicationCommands.Copy;
public static RoutedCommand PasteCommand = ApplicationCommands.Paste;
public static RoutedCommand DeleteCommand = ApplicationCommands.Delete;
public static RoutedCommand FindNextCommand;// Определим в ст. конструкторе
public static RoutedCommand ReplaceCommand = ApplicationCommands.Replace;
public static RoutedCommand GoToCommand;// Определим в ст. конструкторе
public static RoutedCommand SelectAllCommand = ApplicationCommands.SelectAll;
// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
}
}
}
Мы объявили псевдонимы команд как общедоступные поля класса Window1. Команды,
которые имеют встроенные жесты или не должны иметь жестов, мы инициализировали
сразу. Две команды только объявили, но сами объекты собираемся создать в коде.
Код создания этих команд мы поместим в статический конструктор
для добавления жестов. Это нужно для того, что добавление жестов требует действий,
а это разрешено только в методах. Конструктор должен быть обязательно статическим,
чтобы мог выполниться до создания экземпляра окна. Все команды должны к этому
времени уже существовать, поскольку используются в разметке окна при создании
интерфейсных элементов.
Добавьте в класс Window1 файла EnabledControls.cs статический конструктор со следующим кодомnamespace Notepad2
{
partial class Window1
{
..............................................
// Статический конструктор
static Window1()
{
// Определяем с добавлением жестов
InputGestureCollection coll = new InputGestureCollection();
coll.Add(new KeyGesture(Key.F3, ModifierKeys.None, "F3"));
FindNextCommand = new RoutedCommand("FindNext", typeof(Window1), coll);
coll = new InputGestureCollection();
coll.Add(new KeyGesture(Key.G, ModifierKeys.Control, "Ctrl+G"));
GoToCommand = new RoutedCommand("GoTo", typeof(Window1), coll);
}
// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
}
}
}
Не забывайте, что статический конструктор класса в C# не принимает параметров
и может существовать только в единственном экземпляре (если объявим).
Теперь в двух пользовательских командах имеются как жесты, так и вся необходимая информация для отображения в пунктах меню.
В соответствии с планом, присоединим команды к источникам, вначале в коде.
Добавьте в функцию AdditionalHandlers() файла EnabledControls.cs следующий кодvoid AdditionalHandlers()
{
Clipboard.Clear();// Временно, чтобы испытать начальное состояние
// Присоединяем команды к источникам, жесты уже встроены в команды
//SaveCommand - присоединим в разметке
//PageSetupCommand - присоединим в разметке
//UndoCommand - присоединим в разметке
//RedoCommand - присоединим в разметке
//CutCommand - присоединим в разметке
//CopyCommand - присоединим в разметке
btnPaste.Command = itemPaste.Command = contextPaste.Command = PasteCommand;
btnDelete.Command = itemDelete.Command = contextDelete.Command = DeleteCommand;
itemFindNext.Command = FindNextCommand;
itemReplace.Command = ReplaceCommand;
itemGoTo.Command = GoToCommand;
itemSelectAll.Command = SelectAllCommand;
}
Добавьте в открывающем
дескрипторе <Window> файла Window1.xaml параметр отображения пространства
имен класса процедурного кода на разметку, чтобы компилятор видел вставляемые
в разметку команды. Имя отображения можно принять произвольно - выберите myCmdКогда вы начнете вручную набирать запись xmlns:myCmd=,
то после ввода знака присваивания IntelliSense выдаст подсказку, в которой
нужно выбрать выделенную на снимке опцию списка

В результате будет догенерирована следующая запись
<Window x:Class="Notepad2.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:myCmd="clr-namespace:Notepad2"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
Closing="Window_Closing"
Activated="Window_Activated"
>
..............................................
</Window>
Теперь отредактируем разметку 12 запланированных источников задач в соответствии с таблицей
| Где присоединить к источнику? | Задача | Alias (псевдоним) |
|---|---|---|
| Разметка | Save | SaveCommand |
| Разметка | Page Setup | PageSetupCommand |
| Разметка | Undo | UndoCommand |
| Разметка | Redo | RedoCommand |
| Разметка | Cut | CutCommand |
| Разметка | Copy | CopyCommand |
| Paste | PasteCommand | |
| Delete | DeleteCommand | |
| Find Next | FindNextCommand | |
| Replace | ReplaceCommand | |
| Go To | GoToCommand | |
| Select All | SelectAllCommand |
В файле Window1.xaml текущего проекта удалите в источниках для первых 6 задач таблицы
тексты жестов и замените записи с событием Click на записи с присоединением
команд
В файле Window1.xaml текущего проекта удалите в источниках для последних 6 задач таблицы
тексты жестов и записи события Click. Для этих источников присоединение
команд мы уже выполнили в процедурном кодеПравленный код разметки станет таким (для удобства файл Window1.xaml с новым
содержимым приводится полностью)
<Window x:Class="Notepad2.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:myCmd="clr-namespace:Notepad2"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
Closing="Window_Closing"
Activated="Window_Activated"
>
<Window.Resources>
<!-- File -->
<Image x:Shared="False" x:Key="iconNew" Source="Images/NewDocumentHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconOpen" Source="Images/OpenHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconSave" Source="Images/SaveHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPageSetup" Source="Images/PrintSetupHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPrintPreview" Source="Images/PrintPreviewHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPrint" Source="Images/PrintHS.png" Width="16" Height="16" />
<!-- Edit -->
<Image x:Shared="False" x:Key="iconUndo" Source="Images/Edit_UndoHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconRedo" Source="Images/Edit_RedoHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconCut" Source="Images/CutHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconCopy" Source="Images/CopyHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPaste" Source="Images/PasteHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconDelete" Source="Images/DeleteHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconFind" Source="Images/FindHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconFont" Source="Images/FontHS.png" Width="16" Height="16" />
</Window.Resources>
<DockPanel LastChildFill="True">
<!-- Меню -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<!-- Сокращенные варианты подключения иконок с использованием статических ресурсов -->
<MenuItem Name="itemNew" Click="NewOnExecute" Header="_New" InputGestureText="Ctrl+N"
Icon="{StaticResource iconNew}" />
<MenuItem Name="itemOpen" Click="OpenOnExecute" Header="_Open..." InputGestureText="Ctrl+O"
Icon="{StaticResource iconOpen}" />
<MenuItem Name="itemSave" Command="myCmd:Window1.SaveCommand" Header="_Save"
Icon="{StaticResource iconSave}" />
<MenuItem Name="itemSaveAs" Click="SaveAsOnExecute" Header="Save _As..." />
<Separator />
<MenuItem Name="itemPageSetup" Command="myCmd:Window1.PageSetupCommand" Header="Page Set_up..."
Icon="{StaticResource iconPageSetup}" />
<MenuItem Name="itemPrintPreview" Click="PrintPreviewOnExecute" Header="P_rint Preview"
InputGestureText="Ctrl+F2" Icon="{StaticResource iconPrintPreview}" />
<MenuItem Name="itemPrint" Click="PrintOnExecute" Header="_Print..."
InputGestureText="Ctrl+P" Icon="{StaticResource iconPrint}" />
<Separator />
<MenuItem Name="itemExit" Click="ExitOnExecute" Header="E_xit" />
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Name="itemUndo" Command="myCmd:Window1.UndoCommand" Header="_Undo" Icon="{StaticResource iconUndo}" />
<MenuItem Name="itemRedo" Command="myCmd:Window1.RedoCommand" Header="_Redo" Icon="{StaticResource iconRedo}" />
<Separator></Separator>
<MenuItem Name="itemCut" Command="myCmd:Window1.CutCommand" Header="Cu_t" Icon="{StaticResource iconCut}" />
<MenuItem Name="itemCopy" Command="myCmd:Window1.CopyCommand" Header="_Copy" Icon="{StaticResource iconCopy}" />
<MenuItem Name="itemPaste" Header="_Paste" Icon="{StaticResource iconPaste}" />
<MenuItem Name="itemDelete" Header="De_lete" Icon="{StaticResource iconDelete}" />
<Separator></Separator>
<MenuItem Name="itemFind" Click="FindOnExecute" Header="_Find..." InputGestureText="Ctrl+F"
Icon="{StaticResource iconFind}" />
<MenuItem Name="itemFindNext" Header="Find _Next" />
<MenuItem Name="itemReplace" Header="_Replace..." />
<MenuItem Name="itemGoTo" Header="_Go To..." />
<Separator></Separator>
<MenuItem Name="itemSelectAll" Header="Select _All" />
</MenuItem>
<MenuItem Header="F_ormat">
<MenuItem Name="itemFont" Click="FontOnExecute" Header="_Font..." Icon="{StaticResource iconFont}" />
<Separator />
<MenuItem Name="itemWordWrap" Click="WordWrapOnExecute" Header="_Word Wrap" IsCheckable="True"
IsChecked="True" InputGestureText="Ctrl+W" />
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Name="itemAbout" Click="AboutOnExecute" Header="_About" />
</MenuItem>
</Menu>
<!-- Панель инструментов -->
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<Button Name="btnNew" Click="NewOnExecute" Width="23" Content="{StaticResource iconNew}" />
<Button Name="btnOpen" Click="OpenOnExecute" Width="23" Content="{StaticResource iconOpen}" />
<Button Name="btnSave" Command="myCmd:Window1.SaveCommand" Width="23" Content="{StaticResource iconSave}" />
</ToolBar>
<ToolBar>
<Button Name="btnUndo" Command="myCmd:Window1.UndoCommand" Width="23" Content="{StaticResource iconUndo}" />
<Button Name="btnRedo" Command="myCmd:Window1.RedoCommand" Width="23" Content="{StaticResource iconRedo}" />
<Separator />
<Button Name="btnCut" Command="myCmd:Window1.CutCommand" Width="23" Content="{StaticResource iconCut}" />
<Button Name="btnCopy" Command="myCmd:Window1.CopyCommand" Width="23" Content="{StaticResource iconCopy}" />
<Button Name="btnPaste" Width="23" Content="{StaticResource iconPaste}" />
<Button Name="btnDelete" Width="23" Content="{StaticResource iconDelete}" />
</ToolBar>
<ToolBar Header="Find:">
<TextBox Width="100" />
<Button Name="btnFind" Click="FindOnExecute" Width="23" Content="{StaticResource iconFind}" />
</ToolBar>
</ToolBarTray>
<!-- Строка состояния -->
<StatusBar DockPanel.Dock="Bottom" Height="32" Name="statusBar">
<Label>Simulator Application is Loading</Label>
<Separator />
<ProgressBar Height="20" Width="100" IsIndeterminate="True" />
</StatusBar>
<!-- Многострочное текстовое поле редактирования -->
<TextBox TextWrapping="Wrap"
AcceptsReturn="True"
AcceptsTab="True"
VerticalScrollBarVisibility="Auto"
Name="txtBox1"
TextChanged="txtBox1_TextChanged"
HorizontalScrollBarVisibility="Auto"
>
<TextBox.ContextMenu>
<ContextMenu Width="100">
<MenuItem Name="contextCut" Command="myCmd:Window1.CutCommand" Header="Cu_t" Icon="{StaticResource iconCut}" />
<MenuItem Name="contextCopy" Command="myCmd:Window1.CopyCommand" Header="_Copy" Icon="{StaticResource iconCopy}"/>
<MenuItem Name="contextPaste" Header="_Paste" Icon="{StaticResource iconPaste}" />
<MenuItem Name="contextDelete" Header="De_lete" Icon="{StaticResource iconDelete}" />
</ContextMenu>
</TextBox.ContextMenu>
</TextBox>
</DockPanel>
</Window>
Эта разметка визуально получилась достаточно широкой, ее лучше прежде скопировать, а потом разбирать.
Удалите (я закомментировал)
код создания жестов в функции CreateGestures() файла KeyGestures.cs для
выбранных нами 12 задач (для задачи Page Setup жестов нет)void CreateGestures()
{
// File
gests.Add(new KeyGesture(Key.N, ModifierKeys.Control), NewOnExecute);//_New
gests.Add(new KeyGesture(Key.O, ModifierKeys.Control), OpenOnExecute);//_Open...
//gests.Add(new KeyGesture(Key.S, ModifierKeys.Control), SaveOnExecute);//_Save
gests.Add(new KeyGesture(Key.F2, ModifierKeys.Control), PrintPreviewOnExecute);//P_rint Preview
gests.Add(new KeyGesture(Key.P, ModifierKeys.Control), PrintOnExecute);//_Print...
// Edit
//gests.Add(new KeyGesture(Key.Z, ModifierKeys.Control), UndoOnExecute);//_Undo
//gests.Add(new KeyGesture(Key.Y, ModifierKeys.Control), RedoOnExecute);//_Redo
//gests.Add(new KeyGesture(Key.X, ModifierKeys.Control), CutOnExecute);//Cu_t
//gests.Add(new KeyGesture(Key.C, ModifierKeys.Control), CopyOnExecute);//_Copy
//gests.Add(new KeyGesture(Key.V, ModifierKeys.Control), PasteOnExecute);//_Paste
//gests.Add(new KeyGesture(Key.Delete, ModifierKeys.None), DeleteOnExecute);//De_lete
gests.Add(new KeyGesture(Key.F, ModifierKeys.Control), FindOnExecute);//_Find...
//gests.Add(new KeyGesture(Key.F3, ModifierKeys.None), FindNextOnExecute);//Find _Next
//gests.Add(new KeyGesture(Key.H, ModifierKeys.Control), ReplaceOnExecute);//_Replace...
//gests.Add(new KeyGesture(Key.G, ModifierKeys.Control), GoToOnExecute);//_Go To...
//gests.Add(new KeyGesture(Key.A, ModifierKeys.Control), SelectAllOnExecute);//Select _All
// Format
gests.Add(new KeyGesture(Key.W, ModifierKeys.Control), WordWrapOnExecute);//_Word Wrap
}
Запустите приложение
и убедитесь, что тексты жестов во всех источниках команд присутствуют
несмотря на то, что мы их только что явно удалили. Теперь жесты в источники
попадают из командВ начальном состоянии источники команд являются недоступными, поскольку пока
нет привязки к элементам визуального дерева и обработчикам. Исправим это и
привяжем команды к коллекции CommandBindings окна Window1. Причем, в соответствии
с намеченным планом выполним привязку одной части команд в коде, а остальной
- в разметке.
В месте привязки для каждого объекта привязки нужно указать имя команды,
имя обработчика события Executed и имя обработчика события CanExecute для
управления доступностью источников. Оба события всплывающие, поэтому местом
привязки можно выбрать любой элемент маршрута, например, окно Window1. Это
корень визуального дерева и ни одно всплывающее событие мимо него не пройдет.
Обработчики
события Executed для выполнения команд у нас есть, мы их уже создавали для
обработки события Click. Например, для команды Save обработчик имеет имя SaveOnExecute и его сигнатура выглядит так
private void SaveOnExecute(object sender, RoutedEventArgs e)
{
}
Нам предстоит создать обработчики события CanExecute и мы разместим их в файле EnabledControls.cs. Они будут иметь несколько
иную сигнатуру. Например, для команды Save такой обработчик должен иметь следующую
заготовку
private void SaveCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
Добавьте в класс Window1 файла EnabledControls.cs заготовки обработчиков события CanExecute для выбранных
нами ранее 12 команд (число-то какое хорошее!)// Обработчики события CanExecute команд
private void SaveCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void PageSetupCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void UndoCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void RedoCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void CutCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void CopyCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void PasteCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void DeleteCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void FindNextCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void ReplaceCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void GoToCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
private void SelectAllCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
}
Теперь пришла пора выполнить саму привязку в соответствии с намеченным планом
| Где привязать к окну? | Задача | Alias (псевдоним) | Жесты | Регулировать доступность источников? |
|---|---|---|---|---|
| Разметка | Save | SaveCommand | Ctrl+S | Да |
| Разметка | Page Setup | PageSetupCommand | Нет |
|
| Разметка | Undo | UndoCommand | Ctrl+Z | Да |
| Код | Redo | RedoCommand | Ctrl+Y | Да |
| Код | Cut | CutCommand | Ctrl+X | Да |
| Код | Copy | CopyCommand | Ctrl+C | Да |
| Разметка | Paste | PasteCommand | Ctrl+V | Да |
| Разметка | Delete | DeleteCommand | Del | Да |
| Разметка | Find Next | FindNextCommand | F3 | Да |
| Код | Replace | ReplaceCommand | Ctrl+H | Да |
| Код | Go To | GoToCommand | Ctrl+G | Нет |
| Код | Select All | SelectAllCommand | Ctrl+A | Да |
Обратите внимание, что источники команд PageSetupCommand и GoToCommand должны быть доступны всегда, поэтому привяжем для них только обработчики события Executed
Удалите в файле EnabledControls.cs заготовки обработчиков события CanExecute для
команд PageSetupCommand и GoToCommand (или не удаляйте и пусть
болтаются как незадействованные методы)
В файле Window1.xaml текущего проекта выполните привязку части команд к объекту окна, для этого
после открывающего дескриптора окна вставьте следующую разметку<Window x:Class="Notepad2.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:myCmd="clr-namespace:Notepad2"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
Closing="Window_Closing"
Activated="Window_Activated"
>
<!-- Привязка команд к объекту окна -->
<Window.CommandBindings>
<CommandBinding Command="myCmd:Window1.SaveCommand" Executed="SaveOnExecute" CanExecute="SaveCanExecute" />
<CommandBinding Command="myCmd:Window1.PageSetupCommand" Executed="PageSetupOnExecute" />
<CommandBinding Command="myCmd:Window1.UndoCommand" Executed="UndoOnExecute" CanExecute="UndoCanExecute" />
<CommandBinding Command="myCmd:Window1.PasteCommand" Executed="PasteOnExecute" CanExecute="PasteCanExecute" />
<CommandBinding Command="myCmd:Window1.DeleteCommand" Executed="DeleteOnExecute" CanExecute="DeleteCanExecute" />
<CommandBinding Command="myCmd:Window1.FindNextCommand" Executed="FindNextOnExecute" CanExecute="FindNextCanExecute" />
</Window.CommandBindings>
..........................................
</Window>
В файле EnabledControls.cs добавьте в функцию AdditionalHandlers() код привязки части команд к объекту
окна, после чего функция должна стать такой// Вызов размещен в конструкторе класса
void AdditionalHandlers()
{
Clipboard.Clear();// Временно, чтобы испытать начальное состояние
// Присоединяем команды к источникам, жесты уже встроены в команды
//SaveCommand - присоединим в разметке
//PageSetupCommand - присоединим в разметке
//UndoCommand - присоединим в разметке
//RedoCommand - присоединим в разметке
//CutCommand - присоединим в разметке
//CopyCommand - присоединим в разметке
btnPaste.Command = itemPaste.Command = contextPaste.Command = PasteCommand;
btnDelete.Command = itemDelete.Command = contextDelete.Command = DeleteCommand;
itemFindNext.Command = FindNextCommand;
itemReplace.Command = ReplaceCommand;
itemGoTo.Command = GoToCommand;
itemSelectAll.Command = SelectAllCommand;
// Привязка части команд к объекту окна в коде
// 0)
this.CommandBindings.Add(new CommandBinding(RedoCommand, RedoOnExecute, RedoCanExecute));
this.CommandBindings.Add(new CommandBinding(CutCommand, CutOnExecute, CutCanExecute));
this.CommandBindings.Add(new CommandBinding(CopyCommand, CopyOnExecute, CopyCanExecute));
// Теперь чуть подлиннее: создаем, настраиваем, привязываем!
// 1)
CommandBinding binding = new CommandBinding();
binding.Command = ReplaceCommand;
binding.Executed += ReplaceOnExecute;
binding.CanExecute += ReplaceCanExecute;
this.CommandBindings.Add(binding);
// 2)
binding = new CommandBinding(GoToCommand);
binding.Executed += GoToOnExecute;
this.CommandBindings.Add(binding);
// 3)
binding = new CommandBinding(SelectAllCommand, SelectAllOnExecute);
binding.CanExecute += SelectAllCanExecute;
this.CommandBindings.Add(binding);
}
После того, как ссылка binding, которая пока единствественная адресовала
объект, передаст адрес закрепленного за ней объекта в коллекцию CommandBindings элемента,
этот объект станет адресоваться в двух местах: в коллекции и в ссылке. Поэтому
такая ссылка станет уже излишней и ее можно будет использовать для присвоения
адреса нового объекта. А коллекция так и будет продолжать адресовать брошенный
ссылкой объект. В коде показано применение вариантов перегрузок конструктора
класса CommandBinding и
настройки свойств объекта.
Запустите приложение
и убедитесь в следующем...Источники, к которым мы присоединили команды, по прежнему остаются недоступными,
как после того, как мы к ним команды только присоединили. Но заметьте, что
два источника: File/Page Setup... и Edit/Go To... открылись и функционируют,
а для Go To даже клавиатурный жест Ctrl+G действует нормально. Это происходит
потому, что мы для них не зарегистрировали обработчики событий CanExecute и они свободно вызывают обработчики, зарегистрированные в событии Executed,
а остальные источники ждут разрешение на доступность. Создание таких разрешений
в обработчиках CanExecute и составляет суть реализации логики доступности
источников команд.
В соответствии с логикой
работы подобных приложений заполните заготовки обработчиков события CanExecute в файле EnabledControls.cs следующим образом (комментарии в коде)// Обработчики события CanExecute команд
private void SaveCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Позволить, если есть несохраненные изменения
e.CanExecute = IsModified;
}
/*
private void PageSetupCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Источник не регулируется
}
*/
private void UndoCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Позволить, если есть что откатывать
e.CanExecute = IsModified;
}
private void RedoCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Позволить, если очередь отмены есть и стоим не самые первые
e.CanExecute = txtBox1.CanRedo;
}
private void CutCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Позволить, если есть выделенный текст
e.CanExecute = txtBox1.SelectionLength > 0;
}
private void CopyCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Позволить, если есть выделенный текст
e.CanExecute = txtBox1.SelectionLength > 0;
}
private void PasteCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Позволить, если буфер обмена непустой и там содержится текстовый формат
e.CanExecute = Clipboard.ContainsText();
}
private void DeleteCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Позволить, если есть выделенный текст
e.CanExecute = txtBox1.SelectionLength > 0;
}
private void FindNextCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
//!!!!! От фонаря - не хочется думать !!!!!
e.CanExecute = txtBox1.CaretIndex < txtBox1.Text.Length;
}
private void ReplaceCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
//!!!!! От фонаря - не хочется думать !!!!!
e.CanExecute = txtBox1.CaretIndex < txtBox1.Text.Length;
}
/*
private void GoToCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Источник не регулируется
}
*/
private void SelectAllCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Заблокировать, если нечего выделять или уже все выделено
if (txtBox1.Text == String.Empty ||
txtBox1.SelectionLength == txtBox1.Text.Length)
e.CanExecute = false;
else
e.CanExecute = true;
}
Запустите приложение
и убедитесь... - что все работает не так как надо!!!Исправим это. Начнем с журнала откатов, встроенного в элемент TextBox.
По логике, очередь изменений должна очищаться при создании
нового документа или открытии существующего. А эта задача у
нас выполняется в обработчиках NewOnExecute() и OpenOnExecute().
Откройте файл File.cs и добавьте в упомянутые обработчики код очистки txtBox1 следующим образомprivate void NewOnExecute(object sender, RoutedEventArgs e)
{
// Пользователь передумал или была ошибка записи изменений
if (!CheckModifiedAndSaveIt())
return;
// Изменений нет или они успешно сохранены
//txtBox1.Text = String.Empty; // Вариант I
//txtBox1.Text = ""; // Вариант II
txtBox1.Clear(); // Вариант III
strLoadedFile = null;
IsModified = false;
UpdateTitle();
txtBox1.UndoLimit = 0; // Очистка очереди отмены
txtBox1.UndoLimit = -1; // Размер по умолчанию ограничен памятью
txtBox1.Focus();
// Надо отключить Cut, Copy и Delete
}
private void OpenOnExecute(object sender, RoutedEventArgs e)
{
if (DisplayOpenDialog())
{
txtBox1.CaretIndex = txtBox1.Text.Length;// Курсор в конец
txtBox1.UndoLimit = 0; // Очистка очереди отмены
txtBox1.UndoLimit = -1; // Размер по умолчанию ограничен памятью
}
txtBox1.Focus();// Передача фокуса
}
Откройте файл Edit.cs и добавьте в обработчик UndoOnExecute() следующий кодprivate void UndoOnExecute(object sender, RoutedEventArgs e)
{
txtBox1.Undo();
if (!txtBox1.CanUndo)
IsModified = false;
}
Запустите приложение
и убедитесь... - что опять все работает не так как надо!!!Вроде бы все предусмотрели, в чем здесь дело?
Проставьте в файле Edit.cs вдоль левого поля в теле обработчиков
UndoOnExecute(), RedoOnExecute(), CutOnExecute(), CopyOnExecute(),
PasteOnExecute(), DeleteOnExecute(), SelectAllOnExecute()
напротив исполнимого кода точки останова (Breakpoin), как показано на снимке
>
Запустите приложение
и поэксперименируйте с источниками команд - ни один
из отмеченных обработчиков (кроме DeleteOnExecute, да и тот на жест Del не реагирует) не срабатывает, хотя текстовый элемент свои функции выполняет
исправноЭто происходит из-за того, что для соответствующих задач мы использовали
библиотечные команды, которые напрямую работают с текстовыми элементами, имеющими
фокус ввода. Ну работают и пусть себе работают, поскольку в наших обработчиках
предусмотрена та же самая функциональность, кроме UndoOnExecute().
Удалите все точки
останова командой оболочки Debug/Delete All Breakpoints
Теперь в файле EnabledControls.cs проставляйте
поодиночке точки останова и каждый раз запускайте приложение для обработчиков
UndoCanExecute(), RedoCanExecute(), CutCanExecute(), CopyCanExecute(),
PasteCanExecute(), DeleteCanExecute(), SelectAllCanExecute(). Для остановки процесса пользуйтесь командой Stop Debugging меню Debug оболочки или одноименной кнопкой панели инструментов!!!Мы видим, что все эти обработчики события CanExecute срабатывают исправно
для всех команд: и библиотечных, и пользовательских. Причем срабатывают перед
тем, как элемент показывается при раскрытии меню, начальной отрисовке или
наведении курсора. То есть срабатывает именно тогда, когда нужно принять решение,
с какой доступностью этот элемент отобразить.
Итак, для правильной работы логики доступности источников команд нам нужно
заставить выполняться именно наши обработчики для команд Undo, Redo и SelectAll.
Добавьте в статический
конструктор файла EnabledControls.cs следующий код переопределения библиотечных
команд на пользовательские// Статический конструктор
static Window1()
{
// Определяем с добавлением жестов
InputGestureCollection coll = new InputGestureCollection();
coll.Add(new KeyGesture(Key.F3, ModifierKeys.None, "F3"));
FindNextCommand = new RoutedCommand("FindNext", typeof(Window1), coll);
coll = new InputGestureCollection();
coll.Add(new KeyGesture(Key.G, ModifierKeys.Control, "Ctrl+G"));
GoToCommand = new RoutedCommand("GoTo", typeof(Window1), coll);
// Заменяем библиотечные команды на свои для правильной работы логики
coll = new InputGestureCollection();
coll.Add(new KeyGesture(Key.Z, ModifierKeys.Control, "Ctrl+Z"));
UndoCommand = new RoutedCommand("Undo", typeof(Window1), coll);
coll = new InputGestureCollection();
coll.Add(new KeyGesture(Key.Y, ModifierKeys.Control, "Ctrl+Y"));
RedoCommand = new RoutedCommand("Redo", typeof(Window1), coll);
coll = new InputGestureCollection();
coll.Add(new KeyGesture(Key.A, ModifierKeys.Control, "Ctrl+A"));
SelectAllCommand = new RoutedCommand("SelectAll", typeof(Window1), coll);
}
Обратите внимание на то, что мы не удалили в полях инициализацию переопределенных ссылок библиотечными командами. Мы понадеялись на порядок инициализации полей и срабатывания статического конструктора. Вначале вынесенные в поля класса ссылки на команды объявляются и инициализируются, а затем в статическом конструкторе эти же ссылки получают адреса новых команд. Это малоприметный, но очень важный нюанс, проигнорировав который можно получить неуловимую ошибку выполнения.
Это плохая практика программирования, поскольку завтра могут выпустить другой
транслятор или среду исполнения CLR, где этот порядок будет изменен,
и наша программа станет работать неверно. Microsoft не дремлет (!!!) и регулярно посылает
по сети обновления и исправления. А что они завтра пришлют, кто его знает.
Поэтому лучше было бы в полях ссылки только объявить, а уже в конструкторе
они бы наверняка получили адреса наших (пользовательских) команд. Можете так
и сделать, но я у себя оставляю как есть.
Запустите приложение
- теперь все работает как надо!!!Мы спроектировали более-менее сносный текстовый редактор, для этого спукались
на относительно низкий (подробный) уровень программирования. Конечно, в получившемся
продукте еще много недоработок, да и такую задачу можно было бы решить в два
счета на C++Builder. Но мы-то не блокнот проектировали, а знакомились с новым
механизмом команд WPF на примере блокнота.
Вначале мы попытались проектировать без привлечения команд. Создали основу
блокнота, но управление доступностью источников реализовали
только для одной задачи Save, и то с большим трудом. Приходилось пускаться
на всякие ухищрения. Затем часть задач переключили на команды. И сразу удалось
гораздо легче и понятнее реализовать доступность всех источников пользовательского
интерфейса. Выгода от применения механизма команд очевидна.
Чтобы более-менее завершить наше блокнотоподобное приложение, немного подукрасим
его. Добавим в строку состояния, которая у нас имеет имя statusBar,
вывод информации о положении курсора. Опять же, постараемся минимально вмешиваться
в уже созданный код. Для этого все решение разместим в отдельном файле.
Выделите узел текущего
проекта и добавьте командой Project/Add New Item новый файл Code File с именем CaretPosition.cs
Заполните файл CaretPosition.cs следующим кодомusing System;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Controls;
namespace Notepad2
{
// Часть класса главного окна
partial class Window1
{
// Создаем экземпляр и регистрируем обработчики
CaretPosition caretPosition;
private void CreateCaretPosition()
{
// Отображение в StatusBar номера строки и столбца
caretPosition = new CaretPosition();// Создаем объект
caretPosition.TxtBox = txtBox1; // Присоединяем TextBox
// Дополняем StatusBar
this.statusBar.Items.Add(
new System.Windows.Controls.Separator());
this.statusBar.Items.Add(caretPosition.StrLineCol);
// Увековечиваем себя!
this.statusBar.Items.Insert(0, new System.Windows.Controls.Separator());
this.statusBar.Items.Insert(0, "Снетков В.М.");
// Регистрируем обработчик события перемещения каретки
txtBox1.SelectionChanged +=
new RoutedEventHandler(txtBox1_SelectionChanged);
}
// Обработчик инициирует вычисление и отрисовку нового положения
void txtBox1_SelectionChanged(object sender, RoutedEventArgs e)
{
caretPosition.CaretChanged();
}
}
class CaretPosition
{
// Закрытые поля
StatusBarItem strLineCol = new StatusBarItem();
TextBox txtBox = new TextBox();
// Открытые свойства
// Для добавления в строку состояния клиента
public StatusBarItem StrLineCol // Папа '-->'
{
// Только для чтения
get { return strLineCol; }
}
// Для присоединения к TextBox клиента
public TextBox TxtBox // Мама '>--'
{
// Только для записи
set { txtBox = value; }
}
// Вычисляет номер строки
int GetLine()
{
int count = 0;
int pos = 0;
int caretPos = txtBox.SelectionStart + 1; //txtBox1.CaretIndex
while (pos < caretPos)
{
count++; // Счетчик строк
pos = txtBox.Text.IndexOf("\r\n", pos);// \n - перевод строки
if (pos != -1) // Нашли очередную пару
pos += 2; // Сдвигаемся правее найденных
else
break;// Больше нет
}
return count;
}
public void CaretChanged()
{
if (!txtBox.IsFocused)
return;
int posChar = txtBox.CaretIndex;
int line = GetLine();
int column = posChar - txtBox.GetCharacterIndexFromLineIndex(line - 1) + 1;
// Обновляем в строке состояния
strLineCol.Content = String.Format(" Ln {0} \t Col {1}", line, column);
}
}
}
В файле Window1.xaml.cs добавьте в конструктор класса Window1 код вызова функции CreateCaretPosition()public Window1()
{
InitializeComponent();
// Создание жестов
this.CreateGestures();
// Дополнительные обработчики в файле EnabledControls.cs
AdditionalHandlers();
// Отображение в StatusBar номера строки и столбца
// Функция находится в файле CaretPosition.cs
CreateCaretPosition();
}
Запустите приложение
- номера строк и столбцов отображаются. Поэкспериментируйте с функциональностью,
разберитесь с кодомОбратите внимание, как мы из вновь созданного класса CaretPosition подключились
(мама) через свойство TxtBox к существующему объект txtBox1 окна
и извлекаем нужную информацию уже в новом классе. И как созданный в новом
классе элемент строки состояния подключили (папа) обратно к окну. Получился
как бы канал связи, по которому информация из объекта txtBox1 окна поступает
в экземпляр нового класса, а затем в обработанном виде возвращается обратно
в окно для отображения в объекте statusBar.
Снимок блокнота на данный момент будет таким

Видно, что строки и столбцы теперь отображаются в строке состояния, как и
ФИО. Имейте ввиду, что мы изначально приняли тезис, что завернутые строки Word Wrap считаем
продолжением одной и той же строки и позицию каретки отображаем как в длинной
незавернутой строке. Можно переделать, но не в этом суть - как сделали так
и сделали, хотя надо было бы сделать по другому...
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.