Элемент Thumb в WinRT

106

Позже мы будем рассматривать ввод с сенсорного экрана и его использование для манипуляций с объектами на экране. А пока стоит упомянуть элемент управления Thumb, предоставляющий простейшую функциональность сенсорного ввода. Класс Thumb определяется в пространстве имен Windows.UI.Xaml.Controls.Primitives и используется в первую очередь как структурный компонент для элементов управления Slider и Scrollbar.

Элемент управления Thumb генерирует три события в зависимости от перемещения мыши, пера или пальца относительно самого себя: DragStarted, DragDelta и DragCompleted. Событие DragStarted происходит тогда, когда вы прикасаетесь к элементу управления Thumb или подводите указатель мыши к его поверхности и нажимаете кнопку. Соответственно событие DragDelta уведомляет о перемещении пальца или мыши. Оно может использоваться для отслеживания перемещения Thumb (и еще чего угодно). Событие DragCompleted обозначает отпускание пальца или кнопки мыши.

В следующей тестовой программе AlphabetBlocks по периметру располагается серия кнопок с буквами цифрами и знаками препинания. Если щелкнуть на такой кнопке, на экране появляется блок (плитка), который можно перетаскивать пальцем или мышью. Наверное вам захочется отправить блок в полет по экрану резким движением пальца, но он не отреагирует на этот жест - элемент управления Thumb не имеет встроенной поддержки инерционного движения. Для этого вам придется организовать обработку событий имена которых начинаются со слова Manipulation. Для прорисовки блоков класс, производный от UserControl, использует файл XAML в котором определяется квадратный объект с размером 144 пиксела, состоящий из объекта Thumb, вспомогательной графики и TextBlock:

<UserControl ...
    Width="144" Height="144"
    x:Name="root">
    <Grid>
        <Thumb DragStarted="OnThumbDragStarted"
               DragDelta="OnThumbDragDelta"
               Margin="18 18 6 6" />

        <!-- Левая сторона -->
        <Polygon Points="0 6, 12 18, 12 138, 0 126"
                 Fill="#E0C080" />

        <!-- Верхняя сторона -->
        <Polygon Points="6 0, 18 12, 138 12, 126 0"
                 Fill="#F0D090" />

        <!-- Грань -->
        <Polygon Points="6 0, 18 12, 12 18, 0 6"
                 Fill="#E8C888" />

        <Border BorderBrush="{Binding ElementName=root, Path=Foreground}"
                BorderThickness="12"
                Background="#FFE0A0"
                CornerRadius="6"
                Margin="12 12 0 0"
                IsHitTestVisible="False" />

        <TextBlock FontFamily="Courier New"
                   FontSize="156"
                   FontWeight="Bold"
                   Text="{Binding ElementName=root, Path=Text}"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   Margin="12 18 0 0"
                   IsHitTestVisible="False" />
    </Grid>
</UserControl>

Объект Polygon в целом похож на Polyline, но он автоматически замыкает фигуру и заполняет ее кистью, заданной свойством Fill.

У элемента управления Thumb назначены обработчики событий DragStarted и DragDelta. Два элемента, расположенные поверх Thumb - Border и TextBlock - визуально скрывают Thumb, но их свойствам IsHitTestVisible задается значение false, чтобы они не мешали вводу с сенсорного экрана достигнуть Thumb.

Свойство BorderBrush элемента Border связывается со свойством Foreground корневого элемента. Напомню, что свойство Foreground определяется классом Control, наследуется классом UserControl и распространяется по визуальному дереву. Свойство Foreground элемента TextBlock автоматически получает ту же самую кисть. Свойство Text элемента TextBlock связывается со свойством Text элемента управления.

Класс UserControl не имеет свойства Text, что наводит на мысль, что оно определяется классом Block. Файл отделенного кода подтверждает это предположение. Большая часть кода класса посвящена определению свойства Text, поддерживаемого свойством зависимости:

using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;

namespace WinRTTestApp
{
    public sealed partial class Block : UserControl
    {
        private static int zindex;

        private static Block()
        {
            TextProperty = DependencyProperty.Register("Text",
                typeof(string),
                typeof(Block),
                new PropertyMetadata("?"));
        }

        public static DependencyProperty TextProperty { private set; get; }

        public static int ZIndex
        {
            get { return ++zindex; }
        }

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

        public string Text
        {
            set { SetValue(TextProperty, value); }
            get { return (string)GetValue(TextProperty); }
        }

        private void OnThumbDragStarted(object sender, DragStartedEventArgs args)
        {
            Canvas.SetZIndex(this, ZIndex);
        }

        private void OnThumbDragDelta(object sender, DragDeltaEventArgs args)
        {
            Canvas.SetLeft(this, Canvas.GetLeft(this) + args.HorizontalChange);
            Canvas.SetTop(this, Canvas.GetTop(this) + args.VerticalChange);
        }
    }
}

Класс Block также определяет статическое свойство ZIndex, о котором стоит упомянуть особо. Когда вы щелкаете на кнопках, программа создает объекты Block и добавляет их на панель Canvas; при этом каждый последующий объект Block располагается поверх предыдущих объектов. Однако при последующих прикосновениях к блоку этот блок должен «подняться» на верх стопки, то есть его z-индекс должен быть больше, чем у всех остальных блоков.

Нужного эффекта нам поможет добиться статическое свойство ZIndex. Обратите внимание на увеличение значения при каждом вызове. Каждый раз, когда происходит событие DragStarted (то есть пользователь прикоснулся к одному из этих элементов управления), метод Canvas.SetZIndex() назначает Block z-индекс больше, чем у всех остальных. Конечно, теоретически этот процесс прервется при достижении свойством ZIndex максимального возможного значения, но это крайне маловероятно. (Windows Runtime устанавливает произвольно выбранное максимальное значение 1 000 000, так что если вы будете перемещать по одному блоку в секунду без остановки, программа завершится с исключением только на 12-й день.)

Событие DragDelta класса Thumb передает информацию о перемещении точки прикосновения или мыши относительно предыдущего положения в форме свойств HorizontalChange и VerticalChange. Они используются просто для увеличения вложенных свойств Canvas.Left и Canvas.Top.

Файл MainPage.xaml получился коротким. Основную его часть занимает элемент TextBlock для вывода названия программы в центре страницы:

<Page ...>

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"
          SizeChanged="OnGridSizeChanged">

        <TextBlock Text="Алфавитные блоки"
                   FontStyle="Italic"
                   FontWeight="Bold"
                   FontSize="96"
                   TextWrapping="Wrap"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   TextAlignment="Center"
                   Opacity="0.1" />

        <Canvas Name="buttonCanvas" />
        <Canvas Name="blockcanvas" />
    </Grid>
</Page>

Обратите внимание на обработчик SizeChanged элемента Grid. При каждом изменении размера страницы этот обработчик отвечает за повторное создание всех объектов Button и их равномерное распределение по периметру страницы. Он занимает основную часть в файле отделенного кода:

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

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        const double BUTTON_SIZE = 50;
        const double BUTTON_FONT = 18;
        string blockChars = "АБВГДЕЖЗИЙКЛМНОПРСТУФХЧЦШЬЫЪЭЮЯABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!?-+*/%=";
        Color[] colors = { Colors.Red, Colors.Green, Colors.Orange, Colors.Blue, Colors.Purple };
        Random rand = new Random();

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

        private void OnGridSizeChanged(object sender, SizeChangedEventArgs args)
        {
            buttonCanvas.Children.Clear();

            double widthFraction = args.NewSize.Width /
                            (args.NewSize.Width + args.NewSize.Height);
            int horzCount = (int)(widthFraction * blockChars.Length / 2);
            int vertCount = (int)(blockChars.Length / 2 - horzCount);
            int index = 0;

            double slotWidth = (args.NewSize.Width - BUTTON_SIZE) / horzCount;
            double slotHeight = (args.NewSize.Height - BUTTON_SIZE) / vertCount + 1;

            // По верхней стороне
            for (int i = 0; i < horzCount; i++)
            {
                Button button = MakeButton(index++);
                Canvas.SetLeft(button, i * slotWidth);
                Canvas.SetTop(button, 0);
                buttonCanvas.Children.Add(button);
            }

            // Вниз по правой стороне
            for (int i = 0; i < vertCount; i++)
            {
                Button button = MakeButton(index++);
                Canvas.SetLeft(button, this.ActualWidth - BUTTON_SIZE);
                Canvas.SetTop(button, i * slotHeight);
                buttonCanvas.Children.Add(button);
            }

            // Справа налево по нижней стороне
            for (int i = 0; i < horzCount; i++)
            {
                Button button = MakeButton(index++);
                Canvas.SetLeft(button, this.ActualWidth - i * slotWidth - BUTTON_SIZE);
                Canvas.SetTop(button, this.ActualHeight - BUTTON_SIZE);
                buttonCanvas.Children.Add(button);
            }

            // Снизу вверх по левой стороне
            for (int i = 0; i < vertCount; i++)
            {
                Button button = MakeButton(index++);
                Canvas.SetLeft(button, 0);
                Canvas.SetTop(button, this.ActualHeight - i * slotHeight - BUTTON_SIZE);
                buttonCanvas.Children.Add(button);
            }
        }

        private Button MakeButton(int index)
        {
            Button button = new Button
            {
                Content = blockChars[index].ToString(),
                Width = BUTTON_SIZE,
                Height = BUTTON_SIZE,
                FontSize = BUTTON_FONT,
                Tag = new SolidColorBrush(colors[index % colors.Length]),
            };
            button.Click += OnButtonClick;
            return button;
        }

        private void OnButtonClick(object sender, RoutedEventArgs e)
        {
            Button button = sender as Button;

            Block block = new Block
            {
                Text = button.Content as string,
                Foreground = button.Tag as Brush
            };
            Canvas.SetLeft(block, this.ActualWidth / 2 - 144 * rand.NextDouble());
            Canvas.SetTop(block, this.ActualHeight / 2 - 144 * rand.NextDouble());
            Canvas.SetZIndex(block, Block.ZIndex);
            blockcanvas.Children.Add(block);
        }
    }
}

Объект Block создается в обработчике Click элемента управления Button, и ему задается случайное местонахождение где-то неподалеку от центра экрана. Самостоятельно перемещая блоки, пользователь может изобразить очередную версию приветствия для Windows 8.

Программа с возможность перемещения букв
Пройди тесты
Лучший чат для C# программистов