Кнопки в WinRT

59

Среда Windows Runtime поддерживает несколько видов кнопок, производных от класса ButtonBase:

Object
    DependencyObject
        UIElement
            FrameworkElement
                Control
                    ContentControl
                        ButtonBase
                            Button
                            HyperlinkButton
                            RepeatButton
                            ToggleButton
                                CheckBox
                                RadioButton

Следующая программа демонстрирует стандартный внешний вид и функциональность всех этих кнопок:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
     <StackPanel>
            <Button Content="Старая добрая кнопка" />
            <HyperlinkButton Content="HyperlinkButton" />
            <RepeatButton Content="RepeatButton" />
            <ToggleButton Content="ToggleButton" />
            <CheckBox Content="CheckBox" />

            <RadioButton Content="RadioButton #1" />
            <RadioButton>RadioButton #2</RadioButton>
            <RadioButton>
                <RadioButton.Content>
                    RadioButton #3
                </RadioButton.Content>
            </RadioButton>
            <RadioButton>
                <RadioButton.Content>
                    <TextBlock Text="RadioButton #4" />
                </RadioButton.Content>
            </RadioButton>

            <ToggleSwitch />
     </StackPanel>
</Grid>

Я включил четыре экземпляра RadioButton, которые используют разные способы задания свойства Content, но по сути эквивалентны.

Различные кнопки в приложении Windows Runtime

Если вас не устраивает внешний вид какой-либо из кнопок, помните, что вы можете полностью переопределить их при помощи класса ControlTemplate, который будет рассмотрен позже.

Как и у всех классов, производных от FrameworkElement, свойства HorizontalAlignment и VerticalAlignment по умолчанию равны Stretch. Однако к тому моменту, когда кнопка будет загружена, свойству HorizontalAlignment задается значение Left, свойству VerticalAlignment - значение Center, а также установлено ненулевое значение Padding. И хотя свойство Margin равно нулю, кнопка содержит небольшие встроенные поля, окружающие Border.

Класс ButtonBase определяет событие Click, которое инициируется при нажатии и последующем отпускании кнопки (пальцем, мышью или пером). Данное поведение может быть изменено при помощи свойства ClickMode. Кроме того, программа может получать оповещения о щелчке на кнопке при помощи интерфейса команд.

Button - классическая разновидность кнопки. В HyperlinkButton нет ничего особенного, если не считать несколько отличающегося внешнего оформления в результате применения другого шаблона. Кнопка RepeatButton при удержании в нажатом положении генерирует серию событий Click; в основном она предназначена для реализации поведения ScrollBar.

Каждый щелчок на кнопке-переключателе ToggleButton изменяет ее состояние на противоположное (на рисунке кнопка находится в выключенном состоянии). CheckBox просто наследует функциональность ToggleButton, а изменение внешнего вида достигается применением шаблона.

Класс ToggleButton определяет свойство IsChecked для проверки текущего состояния, а также события Checked и Unchecked для оповещения о переходе во включенное и выключенное состояние. Как правило, в программе следует установить обработчики обоих событий, но один обработчик может выполнять обе функции.

Свойство IsChecked класса ToggleButton относится не к типу bool, а к типу Nullable<bool>, это означает, что оно может принимать значение null. При работе с кнопками ToggleButton следует учитывать одну особенность: возможное наличие третьего «неопределенного» состояния. Классический пример флажок (CheckBox) полужирного шрифта в программе форматирования текста. Если выделенный текст оформлен полужирным шрифтом, флажок должен быть установлен, а если простым снят. Однако если выделенный текст содержит как полужирный, так и обычный текст, элемент CheckBox должен отображаться в неопределенном состоянии. Для включения третьего состояния следует задать свойству IsThreeState значение true и назначить обработчик события Indeterminate. У ToggleButton не предусмотрено специальное представление неопределенного состояния; CheckBox вместо «галочки» рисует маленький прямоугольник.

Возможно, с учетом всего сказанного для поддержки функциональности включения/выключения стоит использовать элемент управления ToggleSwitch, потому что он спроектирован специально для приложений Windows 8. И хотя класс ToggleSwitch не является производным от ButtonBase, я все равно включил его в список. Как видно из рисунка, по умолчанию используются метки «Off» и «On», но их можно изменить. Также предусмотрена возможность вывода заголовка.

RadioButton - специальная разновидность ToggleButton для выбора одного варианта из взаимоисключающего набора. По принципу действия этот элемент управления напоминает старые автомобильные радиоприемники с запрограммированными кнопками станций: если нажать кнопку, ранее нажатая кнопка автоматически отключается. Аналогичным образом при установке элемента управления RadioButton снимаются все остальные элементы RadioButton, входящие в ту же группу. От вас потребуется лишь сделать их потомками одной панели. (Будьте внимательны: если поместить RadioButton в элемент Border, он исключается из группы. Чтобы включить Border в визуальное оформление RadioButton, используйте шаблон.) Если потребуется разделить элементы управления RadioButton на несколько взаимоисключающих групп на одной панели, для этого существует свойство GroupName.

Класс Control определяет свойство Foreground, набор шрифтовых свойств и несколько свойств, относящихся к Border; задание этих свойств приводит к изменению внешнего вида кнопки. Допустим, кнопка инициализирована следующим образом:

<Button Content="Старая добрая кнопка"
        Background="DarkOrange"
        Foreground="LimeGreen"
        FontSize="52"
        FontStyle="Italic"
        Padding="20 10"/>
Изменение оформления кнопки

Однако некоторые визуальные характеристики все равно определяются шаблоном. Например, если навести указатель мыши на кнопку или нажать ее, желтый фон моментально исчезает, и фон кнопки окрашивается в стандартные цвета. И хотя вы можете изменить цвет и толщину Border, использовать закругленные углы уже не удастся.

Класс ButtonBase является производным от класса ContentControl, определяющего свойство с именем Content. И хотя в качестве значения Content обычно задается текст, это может быть Image или панель. Безусловно, этот факт открывает массу полезных возможностей. Например, если вы хотите включить в Button растровое изображение и подпись к нему, это делается так:

<Button>
    <StackPanel>
        <Image Source="http://professorweb.ru/my/windows8/rt/level1/files/win8logo.png"
               Width="96" />
        <TextBlock Text="Рисунок 1" HorizontalAlignment="Center" />
    </StackPanel>
</Button>

Свойству Content можно задать практически любой объект и назначить шаблон для представления этого объекта в нужном виде.

В следующем примере мы реализуем простую телефонную клавиатуру. Клавиши представляются элементами управления Button, а вводимый номер отображается в TextBlock.

В следующем файле XAML клавиатура заключена в элемент Grid, свойствам HorizontalAlignment и VerticalAlignment которого задано значение Center, чтобы он располагался в центре экрана. Независимо от размера клавиатуры и содержимого кнопок клавиатура должна состоять из 12 кнопок одинакового размера. Проблемы с шириной и высотой кнопок решались двумя разными способами. Ширина 300 устанавливается для элемента Grid. Конкретная ширина использовалась из-за того, что пользователь может ввести номер произвольной длины, и я не хотел, чтобы клавиатура автоматически расширялась для увеличенного поля TextBlock. Однако высота (Height) каждого объекта Button задается неявным стилем:

<Page ...>

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">

        <Grid HorizontalAlignment="Center"
              VerticalAlignment="Center"
              Width="300">

            <Grid.Resources>
                <Style TargetType="Button">
                    <Setter Property="ClickMode" Value="Press" />
                    <Setter Property="HorizontalAlignment" Value="Stretch" />
                    <Setter Property="Height" Value="72" />
                    <Setter Property="FontSize" Value="36" />
                </Style>
            </Grid.Resources>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>

            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>            

            <Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>

                <Border Grid.Column="0"
                        HorizontalAlignment="Left">

                    <TextBlock Name="resultText"
                               HorizontalAlignment="Right"
                               VerticalAlignment="Center"
                               FontSize="24" />
                </Border>

                <Button Name="deleteButton"
                        Content="&#x21E6;"
                        Grid.Column="1"
                        IsEnabled="False"
                        FontFamily="Segoe Symbol"
                        HorizontalAlignment="Left"
                        Padding="0"
                        BorderThickness="0"
                        Click="OnDeleteButtonClick" />
            </Grid>

            <Button Content="1"
                    Grid.Row="1" Grid.Column="0"
                    Click="OnCharButtonClick" />

            <Button Content="2"
                    Grid.Row="1" Grid.Column="1"
                    Click="OnCharButtonClick" />

            <Button Content="3"
                    Grid.Row="1" Grid.Column="2"
                    Click="OnCharButtonClick" />

            <Button Content="4"
                    Grid.Row="2" Grid.Column="0"
                    Click="OnCharButtonClick" />

            <Button Content="5"
                    Grid.Row="2" Grid.Column="1"
                    Click="OnCharButtonClick" />

            <Button Content="6"
                    Grid.Row="2" Grid.Column="2"
                    Click="OnCharButtonClick" />

            <Button Content="7"
                    Grid.Row="3" Grid.Column="0"
                    Click="OnCharButtonClick" />

            <Button Content="8"
                    Grid.Row="3" Grid.Column="1"
                    Click="OnCharButtonClick" />

            <Button Content="9"
                    Grid.Row="3" Grid.Column="2"
                    Click="OnCharButtonClick" />

            <Button Content="*"
                    Grid.Row="4" Grid.Column="0"
                    Click="OnCharButtonClick" />

            <Button Content="0"
                    Grid.Row="4" Grid.Column="1"
                    Click="OnCharButtonClick" />

            <Button Content="#"
                    Grid.Row="4" Grid.Column="2"
                    Click="OnCharButtonClick" />
        </Grid>
    </Grid>
</Page>

Больше всего проблем создает первая строка. В ней должен находиться элемент TextBlock для вывода введенного номера, а также кнопка удаления. Я не хотел делать кнопку удаления слишком большой, поэтому вся первая строка Grid была выделена для хранения отдельного элемента Grid всего для двух элементов. Атрибуты кнопки удаления переопределяют многие свойства, заданные в неявном стиле. Обратите внимание: кнопка удаления изначально заблокирована. Она должна стать доступной только после того, как в текстовом поле появятся символы.

С TextBlock тоже пришлось повозиться. Я хотел, чтобы текст в процессе ввода выравнивался по левому краю, но если строка окажется слишком длинной для отображения, содержимое TextBlock должно усекаться слева, а не справа. Для этого элемент TextBlock был заключен в Border:

<Border Grid.Column="0"
      HorizontalAlignment="Left">

     <TextBlock Name="resultText"
                   HorizontalAlignment="Right"
                   VerticalAlignment="Center"
                   FontSize="24" />
</Border>

На ширину Border накладывается ограничение: она не может превысить ширины Grid, уменьшенной на ширину кнопки удаления. В пределах этой области элемент Border выравнивается по левому краю. Его размеры подгоняются под размеры TextBlock, поэтому, несмотря на задание свойства HorizontalAlignment, элемент TextBlock также размещается слева.

По мере ввода символов ширина TextBlock увеличивается, пока не превысит ширину Border. В этот момент начинает работать свойство HorizontalAlignment, равное Right, а левая часть TextBlock усекается.

После верхней строки все остальное проще простого. Неявный стиль помогает свести к минимуму объем разметки каждой из 12 кнопок. Файл отделенного кода обрабатывает событие Click кнопки удаления и использует общий обработчик для остальных 12 кнопок:

using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        string inputString = "";
        char[] specialChars = { '*', '#' };

        public MainPage()
        {
            this.InitializeComponent();
        }

        private void charButton_Click(object sender, RoutedEventArgs args)
        {
            Button btn = sender as Button;
            inputString += btn.Content as string;
            FormatText();
        }

        private void deleteButton_Click(object sender, RoutedEventArgs e)
        {
            inputString = inputString.Substring(0, inputString.Length - 1);
            FormatText();
        }

        private void FormatText()
        {
            bool hasNonNumbers = inputString.IndexOfAny(specialChars) != -1;

            if (hasNonNumbers || inputString.Length < 4 || inputString.Length > 10)
                resultText.Text = inputString;

            else if (inputString.Length < 8)
                resultText.Text = String.Format("{0}-{1}", inputString.Substring(0, 3),
                                                           inputString.Substring(3));
            else
                resultText.Text = String.Format("({0}) {1}-{2}", inputString.Substring(0, 3),
                                                                 inputString.Substring(3, 3),
                                                                 inputString.Substring(6));
            deleteButton.IsEnabled = inputString.Length > 0;
        }
    }
}

Обработчик кнопки удаления исключает символ из поля inputString, а другой обработчик события добавляет введенный символ. Затем каждый обработчик вызывает метод FormatText(), который пытается отформатировать строку как телефонный номер. В конце работы метода кнопка удаления становится доступной только в том случае, если входная строка содержит символы.

Создание экранной клавиатуры для ввода номера телефона

Обработчик события OnCharButtonClick по свойству Content нажимаемой кнопки определяет, какой символ следует добавить в строку. Подобная простая эквивалентность между содержимым Content кнопки и ее функциональностью существует не всегда. В некоторых ситуациях для совместного использования несколькими элементами управления обработчик должен получать больше информации о нажатой кнопке. Специально для этой цели FrameworkElement определяет свойство Tag типа object.

В файле XAML свойству Tag можно назначить строковый идентификатор или объект, который будет проверяться в обработчике события. Этот прием будет продемонстрирован в следующей статье.

Пройди тесты
Лучший чат для C# программистов