Поскольку мы не будем использовать эти средства, имеет смысл скрыть панель. Для этого следует выполнить команду меню Tools | Options, в появившемся диалоговом окне Options выбрать раздел Debugging и в этом разделе снять флажок Enable UI Debugging Tools for XAML.
Теперь мы хотим связать определенное действие с нажатием кнопки button1. Для этого можно выполнить следующие шаги:
1) выделите в окне дизайнера кнопку button1;
2) в окне Properties перейдите к разделу со списком событий, нажав на кнопку с изображением молнии: ;
3) выберите в разделе со списком событий строку Click и выполните на ее пустом поле ввода двойной щелчок мышью;
4) в результате активизируется вкладка редактора с файлом MainWindow.xaml.cs, где появится заготовка для нового метода класса MainWindow – обработчик события Click для компонента button1:
5) в эту заготовку надо ввести код, который будет выполняться при нажатии кнопки button1; мы добавим в нее единственный оператор:
Заметим, что соответствующее изменение будет внесено и в xaml-файл:
Именно благодаря заданию атрибута Click в xaml-файле метод button1_Click будет связан с событием Click компонента button1 (при отсутствии такого атрибута метод button1_Click будет считаться обычным методом класса, для выполнения которого требуется его явный вызов).
Описанный выше способ создания нового обработчика события был реализован еще для библиотеки Windows Forms. Однако в WPF-проекте имеется более быстрый способ определения нового обработчика, не требующий использования окна Properties. Необходимо ввести имя события как атрибут соответствующего элемента в xaml-файле (в нашем случае в элемент Button надо ввести текст «Click=», причем достаточно набрать несколько начальных символов имени события и воспользоваться для завершения набора выпадающим списком) и после появления рядом с набранным атрибутом выпадающего списка с текстом «New Event Handler» выбрать этот текст (если он еще не выбран) и нажать клавишу Enter. При этом в xaml-файл будет добавлено имя обработчика (в нашем случае button1_Click), а в cs-файле будет создана заготовка для обработчика с этим именем, хотя перехода к ней не произойдет, чтобы дать возможность продолжить редактирование xaml-файла. Если в программе уже имеются обработчики, совместимые с тем событием, имя которого введено в xaml-файле, то в выпадающем списке наряду с вариантом «New Event Handler» будут приведены и имена всех таких обработчиков, что позволит быстро связать события для нескольких компонентов с одним обработчиком (хотя для подобного связывания имеется более удобная возможность, основанная на механизме маршрутизируемых событий и описанная в проекте CALC).
Если для какого-либо компонента предполагается определять обработчики, то рекомендуется предварительно задать имя для этого компонента, чтобы оно включалось в имена созданных обработчиков.
В дальнейшем вместо детального описания действий по созданию обработчиков событий мы будем просто приводить измененный фрагмент xaml-файла с новыми атрибутами и текст самого обработчика, выделяя добавленные (или измененные) фрагменты полужирным шрифтом:
Текст button1_Click мы не только выделяем полужирным шрифтом, но и подчеркиваем, чтобы отметить то обстоятельство, что этот текст будет автоматически сгенерирован редактором xaml-файлов после ввода текста Click= и выбора из появившегося списка варианта «New Event Handler» (напомним, что при этом в cs-файле будет создан новый обработчик с указанным именем).
Добавим к нашему проекту еще один обработчик – на этот раз для компонента Canvas (обратите внимание на то, что если обработчик создается для компонента, не имеющего имени, то в имени обработчика по умолчанию используется имя класса этого компонента):
Кроме того, необходимо задать фон для компонента Canvas:
Результат. Теперь после запуска программы при щелчке на любом месте окна кнопка «Закрыть» услужливо прыгает на указанное место. Нажатие на кнопку «Закрыть» приводит к завершению программы и возврату в среду Visual Studio.
Комментарии
1. Любой компонент WPF реагирует на нажатие мыши только в том случае, если имеет непустой фон. По умолчанию фон некоторых компонентов является пустым (в окне Properties в этом случае свойство Background имеет значение No Brush или вообще не содержит никакого текста). Следует заметить, что, несмотря на вид атрибута Background в xaml-файле, фон определяется не цветом, а особым классом WPF – кистью (имеется абстрактный класс Brush, от которого порождается несколько классов-потомков). Указание цветовой константы означает, что будет использоваться сплошная кисть типа SolidBrush с заливкой данного цвета. В последних проектах, описанных в данной книге (TRIGFUNC и HTOWERS), мы познакомимся с градиентной кистью, имеющей две разновидности – LinearGradientBrush и RadialGradientBrush.
2. Важной характеристикой любого события, связанного с мышью, является позиция мыши. Для определения этой позиции в обработчиках мыши предусмотрен специальный метод, вызываемый для второго параметра обработчика e: e.GetPosition. Данный метод имеет обязательный параметр, задающий компонент, относительно которого определяется позиция мыши. Мы указали параметр this; это означает, что позиция будет определяться относительно левого верхнего угла клиентской области окна. Заметим, что все размеры в WPF задаются в так называемых аппаратно-независимых единицах (одна единица равна 1/96 дюйма) и представляются в виде вещественных чисел типа double (в то время как в библиотеке Windows Forms размеры задавались в экранно-зависимых пикселах и представлялись целыми числами).
3. Два последних оператора в обработчике Canvas_MouseDown демонстрируют способ, позволяющий задать в программе присоединенные свойства Left и Top, полученные компонентом от его родителя типа Canvas. Обратите внимание на то, что методы SetLeft и SetTop являются статическими и должны вызываться не для конкретного объекта типа Canvas, а для самого класса. Имеются парные методы Canvas.GetLeft(c) и Canvas.GetTop(c), позволяющие определить текущие значения свойств Left и Top для компонента c, расположенного на компоненте Canvas (эти методы будут использованы во фрагменте программы, добавленном в п. 1.5 при исправлении недочета).
Для задания присоединенных свойств можно также использовать «универсальный» метод SetValue, имеющийся у всех компонентов. Например, два последних оператора в обработчике Canvas_MouseDown можно изменить следующим образом:
Обратите внимание на то, что при указании присоединенного свойства в методе SetValue надо использовать его имя с суффиксом Property (на самом деле это и есть «настоящее» имя статического присоединенного свойства, поскольку подобный суффикс имеют все статические свойства зависимости). Интересно отметить, что с помощью метода SetValue с компонентом можно связывать любые свойства зависимости, определенные у любых типов компонентов (для получения значений этих свойств предназначен метод GetValue, пример использования которого приводится в последнем комментарии к п. 1.5).
4. Для определения текущих размеров компонента в программе надо обращаться к свойствам ActualWidth и ActualHeight. Свойства Width и Height для этого использовать нельзя, так как они обычно содержат лишь «рекомендованные» значения размеров, которые учитываются группирующими компонентами при компоновке своих дочерних компонентов (в частности, возможны рекомендованные значения «бесконечность» или NaN). В нашем случае свойства ActualWidth и ActualHeight кнопки используются для того, чтобы отцентрировать кнопку относительно курсора мыши.
В начало описания класса MainWindow (перед конструктором public MainWindow()) добавьте новое поле:
В окно добавьте новую кнопку button2, сделайте ее свойство Content пустой строкой и определите для этой кнопки два обработчика:
Результат. «Дикая» кнопка с пустым заголовком не дает на себя нажать, «убегая» от курсора мыши. Для того чтобы ее «приручить», надо переместить на нее курсор, держа нажатой клавишу Ctrl. После щелчка на дикой кнопке она приручается: на ней появляется заголовок «Изменить», и она перестает убегать от курсора мыши. Следует заметить, что приручить кнопку можно и с помощью клавиатуры, переместив на нее фокус с помощью клавиш со стрелками (или клавиши Tab) и нажав на клавишу пробела.
Недочет. Если попытаться «приручить» кнопку, переместив на нее фокус и нажав клавишу пробела, то перед приручением она прыгает по окну, пока не будет отпущена клавиша пробела. Причины такого поведения непонятны, поскольку нажатие клавиши пробела не должно приводить к активизации события, связанного с перемещением мыши. Следует, однако, отметить, что нажатие пробела обрабатывается в WPF особым образом, и по этой причине оно может приводить к таким странным эффектам.
Исправление. Дополните условие в методе button2_MouseMove:
Прирученная кнопка пока ничего не делает. Это будет исправлено в следующем пункте.
Комментарии
1. В данном пункте демонстрируется возможность отсоединения метода-обработчика от события, с которым он ранее был связан. Для этого используется операция –=, слева от которой указывается событие, а справа – тот обработчик, который надо отсоединить от события.
2. В обработчике button2_MouseMove определяются текущие размеры компонента Canvas, чтобы обеспечить случайное перемещение дикой кнопки только в пределах этого компонента (метод r.NextDouble() возвращает случайное вещественное число в полуинтервале [0; 1), при этом вычитание числа 5 гарантирует, что дикая кнопка будет видна на экране хотя бы частично). Заметим, что программа правильно реагирует на изменение размера окна: дикая кнопка всегда перемещается в пределах его текущего размера. Это обеспечивается благодаря тому, что панель Canvas по умолчанию занимает всю клиентскую область своего родителя-окна.
Поскольку мы не присвоили компоненту Canvas имя, нам пришлось обращаться к нему через его родителя, вызвав для окна его свойство Content и, кроме того, выполнив явное приведение типа с помощью операции as. Вместо приведения к типу Canvas можно было бы выполнить приведение к типу FrameworkElement – первому типу в иерархии наследования компонентов, в котором определены свойства, связанные с размерами. Можно было выполнить приведение к типу Panel – непосредственному потомку FrameworkElement, который является предком всех группирующих компонентов. Заметим, что выполнить приведение класса Canvas к типу Control не удастся, так как группирующие компоненты к данному типу не относятся (потомками Control являются, в частности, компоненты, имеющие свойство Content, например кнопки).
3. Следует обратить внимание на способ, с помощью которого в обработчике button2_MouseMove проверяется, нажата ли клавиша Ctrl. Обычно дополнительная информация о произошедшем событии передается в обработчик с помощью второго параметра e. Например, в обработчике button2_MouseMove с помощью данного параметра (типа MouseEventArgs) можно определить текущую позицию мыши (вызвав метод e.GetPosition) или состояние кнопок мыши (вызвав, например, свойство e.LeftButton и сравнив его с одним из его возможных значений – MouseButtonState.Pressed или MouseButtonState.Released). Однако информацию о нажатых в данный момент клавишах параметр e типа MouseEventArgs не содержит. Тем не менее подобную информацию можно получить с помощью статического свойства IsKeyDown класса Keyboard. Это же свойство мы использовали при исправлении обнаруженного недочета, чтобы в случае, если нажата клавиша пробела, обработчик button2_MouseMove не выполнял никаких действий.
4. Обратите также внимание на то, что поле r в классе WainWinbdow не только описывается, но и сразу инициализируется (с помощью конструктора Random() без параметров). В какой момент выполняется данная инициализация? По правилам языка C#, явно указанные операторы инициализации всех полей класса автоматически помещаются в начало любого конструктора данного класса. Таким образом, поле r будет инициализировано в начале выполнения конструктора окна (перед выполнением оператора InitializeComponent(), указанного в теле данного конструктора). Разумеется, можно было бы поступить иначе: описать поле r типа Random без его инициализации
и поместить в конструктор окна оператор
Для того чтобы прирученная кнопка при нажатии на нее выполняла какие-либо действия, можно добавить эти действия к уже имеющемуся обработчику button2_Click. Однако в этом случае обработчик должен проверять, в каком состоянии находится кнопка – диком или прирученном. Поступим по-другому: свяжем событие Click для прирученной кнопки с новым обработчиком. Такой подход позволит продемонстрировать в нашем проекте ряд особенностей, связанных с действиями по присоединению и отсоединению обработчиков.
Новый обработчик (назовем его button2_Click2) создадим «вручную», не прибегая к услугам окна Properties или xaml-файла. Для этого в конце описания класса MainWindow в файле MainWindow.xaml.cs (перед двумя последними скобками «}») добавим описание этого обработчика:
Чтобы подчеркнуть, что в данном случае никакая часть обработчика не создается автоматически, мы выделили весь текст обработчика полужирным шрифтом.
В метод button2_Click добавьте следующие операторы (здесь и далее в книге предполагается, что если место добавления не уточняется, то операторы надо добавлять в конец метода):
В метод Canvas_MouseDown добавьте операторы:
Результат. Прирученная кнопка теперь выполняет полезную работу – щелчок на ней приводит к разворачиванию окна программы на весь экран, а новый щелчок восстанавливает первоначальное состояние окна. Если же щелкнуть мышью на окне (не на кнопке), то услужливая кнопка «Закрыть» прибежит на вызов, а прирученная кнопка «Изменить» снова одичает, потеряет текст своего заголовка и начнет убегать от мыши.
Комментарий
Приведенные тексты методов показывают, что при смене обработчика недостаточно присоединить к событию новый обработчик; необходимо также отсоединить от события обработчик, ранее связанный с ним. Данное обстоятельство обусловлено тем важным фактом, что к одному и тому же событию можно последовательно присоединить несколько обработчиков (для этого достаточно применить к этому событию несколько раз оператор +=). Следует отметить, что данная возможность для событий визуальных компонентов применяется крайне редко (достаточно отметить, что с помощью окна Properties или xaml-файла присоединить к одному событию несколько обработчиков нельзя). В то же время при явном присоединении обработчиков эта особенность может приводить к появлению трудно выявляемых ошибок, если, например, один и тот же обработчик будет присоединен к событию несколько раз. Подобные проблемы можно проиллюстрировать с помощью нашей программы, если закомментировать заголовок оператора if в обработчике Canvas_MouseDown:
Если теперь после запуска программы несколько раз щелкнуть мышью на окне, а затем приручить кнопку button2, то при ее последующем нажатии окно несколько раз последовательно перейдет из развернутого состояния в стандартное и обратно. Это объясняется тем, что теперь каждый щелчок на окне присоединяет к событию Click кнопки button2 новый экземпляр обработчика button2_Click, и при нажатии на эту кнопку каждый экземпляр обработчика последовательно запускается на выполнение. Ситуация осложняется еще тем обстоятельством, что в программе невозможно выяснить, сколько и какие обработчики присоединены в настоящий момент к данному событию (а не зная этого, нельзя и обеспечить отсоединение от события всех его обработчиков).
Итак, к действиям по явному присоединению обработчика к событию и его последующему отсоединению следует подходить крайне осторожно.
Недочет. При выполнении программы может возникнуть ситуация, когда одна или обе кнопки не будут отображаться в окне (если, например, кнопки были перемещены на новое место при развернутом окне, после чего окно возвращено в исходное состояние).
Исправление. Определите для окна обработчик события SizeChanged:
Результат. Теперь в ситуации, когда при изменении размера окна его кнопки оказываются вне клиентской части, происходит перемещение этих кнопок на исходные позиции около левого верхнего угла окна.
Комментарии
1. В данном обработчике демонстрируется еще один способ доступа к компонентам окна, который удобен для организации перебора в цикле компонентов с похожими именами. Этот способ основан на применении метода FindName, который можно вызывать непосредственно для окна. Метод FindName возвращает компонент окна с указанным именем (или null, если компонент с таким именем в окне отсутствует).
2. Вместо статического метода GetLeft для получения значения присоединенного свойства Left можно было бы использовать более длинный, но и более универсальный вариант, использующий метод GetValue того компонента, к которому ранее было присоединено свойство: (double)b.GetValue(Canvas.LeftProperty). Аналогичным образом можно получить значение свойства Top (и любых других свойств зависимости, присоединенных к данному компоненту).
Рис. 7. Окна приложения WINDOWS
После создания проекта к нему необходимо добавить два дополнительных окна. Для этого требуется выполнить команду Project | Add Window… и в появившемся диалоговом окне указать имя класса, который будет связан с новым окном. Достаточно использовать имена, предлагаемые по умолчанию – Window1 для первого окна, Window2 для второго.
Рис. 8. Макет окна MainWindow приложения WINDOWS
MainWindow.xaml (рис. 8):
Window1.xaml:
Window2.xaml:
В файле MainWindow.xaml.cs в начало описания класса MainWindow добавьте операторы:
Определите обработчики для класса MainWindow (эти обработчики указаны в файле MainWindow.xaml, и поэтому их заготовки уже должны содержаться в классе MainWindow; напомним, что для большей наглядности мы подчеркиваем в xaml-файле имена подобных обработчиков):
Результат. Программа включает три окна, демонстрирующие основные типы окон в графических Windows-приложениях: окно фиксированного размера (MainWindow), окно переменного размера (win1 типа Window1), диалоговое окно (win2 типа Window2). Главное окно MainWindow сразу отображается на экране при запуске приложения. Окна win1 и win2 (подчиненные окна) вызываются из главного окна нажатием соответствующей кнопки. При этом окно win1 отображается в обычном, а окно win2 – в модальном (диалоговом) режиме (если некоторое окно в приложении находится в диалоговом режиме, то до его закрытия нельзя переключаться на другие окна). Для завершения программы надо закрыть ее главное окно. При отображении главного окна место для его размещения выбирается операционной системой, окно win1 отображается около правого нижнего угла главного окна с небольшим наложением, окно win2 отображается в центре экрана.
Следует заметить, что полученная программа содержит серьезную ошибку, которая будет исправлена в следующем пункте.
Комментарии
1. Благодаря явному заданию значения false для свойств ShowInTaskbar подчиненных окон, кнопки для этих окон не отображаются на панели задач в нижней части экрана.
2. За возможность изменения размеров окна и отображение кнопок минимизации/максимизации на его заголовке отвечает свойство ResizeMode, которое может принимать следующие значения: NoResize (размер окна фиксирован, кнопки не отображаются), CanMinimize (размер окна фиксирован, доступна кнопка минимизации), CanResize (значение по умолчанию: окно может менять размер, доступны обе кнопки), CanResizeWithGrip (то же, что и CanResize, но в правом нижнем углу окна дополнительно отображается треугольный маркер; благодаря этому маркеру увеличивается область, которую можно зацепить мышью для изменения размеров окна). Для диалоговых окон дополнительно следует установить свойство WindowStyle равным ToolWindow; это обеспечивает скрытие иконки на заголовке окна (отображать иконки в диалоговых окнах не принято).
3. Присваивание свойству Owner некоторого окна w1, значения какого-либо другого окна w0 делает окно w1 подчиненным по отношению к главному окну w0. Подчиненное окно всегда отображается поверх главного (даже если главное окно является активным). Кроме того, при минимизации или закрытии главного окна его подчиненные окна также минимизируются (или, соответственно, закрываются). Следует заметить, что свойству Owner можно присвоить значение только такого окна, которое уже отображено на экране, поэтому указанные действия мы выполняем в обработчике события Loaded, которое возникает при первом отображении окна.
4. За начальное расположение окна на экране отвечает свойство WindowStartupLocation, равное по умолчанию значению Manual. При этом позицию окна можно задать явно с помощью свойств Left и Top или не задавать эти свойства, оставив определение начальной позиции на усмотрение операционной системы. В последнем случае «истинные» значения свойств Left и Top будут доступны только в момент первого отображения окна на экране. Как уже было отмечено в предыдущем комментарии, с этой ситуацией связано событие окна Loaded, поэтому начальное положение окна win1 определяется нами в обработчике данного события для главного окна.
5. Содержимое файла MainWindow.xaml демонстрирует традиционный для технологии WPF динамический способ размещения компонентов в окне, при котором их размеры и положение (а также иногда и размеры окна) определяются автоматически на основе указанных настроек. В данном случае в окне надо разместить две кнопки по вертикали. Такой способ размещения проще всего обеспечить с помощью группирующего компонента StackPanel (данный компонент имеет свойство Orientation с вариантами значений Vertical и Horizontal, причем первый вариант является значением по умолчанию).
Для указания полей – промежутков между компонентами – используется свойство Margin, которое может состоять из 1, 2 или 4 значений. Единственное значение определяет одинаковое поле (в аппаратно-независимых единицах, равных 1/96 дюйма) во всех направлениях, при наличии двух значений первое определяет поле слева и справа, а второе – сверху и снизу, при наличии четырех значений поля определяются в следующем порядке: левое, верхнее, правое, нижнее. Обратите внимание на то, что для того, чтобы обеспечить одинаковые промежутки (равные 10 единицам) как между компонентами, так и между компонентом и границей окна, следует задать поля, равные 5, как для группирующего (невидимого) компонента, так и для содержащихся в нем видимых компонентов-кнопок.
Помимо «внешних полей» (margins) для компонентов можно задавать «внутренние поля» (paddings), определяющие расстояние от границы компонента до его содержимого. Внутренние поля определяются свойством Padding, которое задается по тем же правилам, что и свойство Margin.
Следует также обратить внимание на значение свойства MinWidth, которое задано только для первой кнопки. Оно определяет минимальную ширину данного компонента и тем самым минимальную ширину всей панели StackPanel, причем все остальные компоненты на этой панели будут иметь такую же ширину. Таким образом, реальные размеры как кнопок, так и панели будут определяться размером шрифта, используемого для надписей на кнопках. Если шрифт велик настолько, что текст по ширине будет превосходить указанную минимальную ширину в 200 единиц, то свойство MinWidth будет проигнорировано и ширина кнопки станет больше 200 единиц; при этом кнопка по-прежнему будет иметь указанные внутренние и внешние поля.
В окнах, подобных главному окну из нашего проекта, желательно, чтобы их размер подстраивался под размер содержимого (в данном случае – панели StackPanel). Для этого предусмотрено свойство окна SizeToContent, которое мы положили равным WidthAndHeight (можно также подстраивать под размер содержимого только ширину или только высоту окна). По умолчанию данное свойство равно Manual, в этом случае не окно подстраивается под свое содержимое, а наоборот – компоненты подстраиваются под размер окна. Заметим, что если оставить в xaml-файле атрибуты Width и Height для окна, то в окне дизайнера окно будет иметь указанные размеры даже при наличии атрибута SizeToContent, равного WidthAndHeight, однако при выполнении программы явно указанные размеры окна будут игнорироваться.