Сенсорный ввод в WinRT

97

Одной из самых перспективных особенностей Windows Runtime стала консолидация ввода с сенсорного экрана, мыши и пера. Теперь разработчику не нужно добавлять поддержку сенсорного ввода в существующее приложение, ориентированное на работу с мышью, или добавлять поддержку мыши в приложение с сенсорным вводом. С самого начала программист использует все эти формы ввода как в целом взаимозаменяемые. В соответствии с терминологией интерфейса программирования Windows Runtime я буду использовать слово «указатель» (pointer) для обозначения ввода с сенсорного экрана, мыши и пера (также называемого стилусом) в тех случаях, когда различать фактическое устройство ввода не обязательно.

Для обработки ввода от указателя лучше всего использовать существующие элементы управления Windows Runtime. Как вы уже видели, все стандартные элементы управления — Button, Slider, ScrollViewer и Thumb — реагируют на ввод от указателя и используют его для передачи приложению входных данных более высокого уровня. Однако в некоторых случаях, программисту бывает нужно получить непосредственный ввод от указателя. Для этих целей UIElement определяет три семейства событий:

Класс Control дополняет эти события виртуальными защищенными методами, имена которых начинаются с префикса On, за которым следует имя события.

Для получения ввода от указателя у класса, производного от FrameworkElement, свойство IsHitTestVisible должно быть равно true, а свойство Visibility должно быть равно Visible. У класса, производного от Control, свойство IsEnabled должно быть равно true. Элемент должен иметь графическое представление на экране; фон класса, производного от Panel, может быть прозрачным, но не равным null.

Все эти события ассоциируются с элементом, находящимся под пальцем, мышью или пером на момент возникновения события. Единственное исключение — «захват» указателя элементом.

Для отслеживания отдельных пальцев следует использовать события Pointer. К каждому событию прилагается идентификатор, однозначно определяющий источник ввода — конкретный палец, мышь или перо. В этой и последующих статьях я покажу, как использовать события Pointer для рисования пальцем на экране и имитации клавиатуры пианино (к сожалению, без звука). Разумеется, обе программы должны обрабатывать одновременный ввод от нескольких пальцев.

В каком-то смысле можно обойтись только событиями Pointer. Например, чтобы реализовать функцию «растяжения» фотографий двумя пальцами, можно отслеживать два события Pointer для этих пальцев и оценить величину их расхождения. Однако вычисления такого рода уже реализованы в событиях Manipulation. Эти события, объединяющие действия нескольких пальцев в одну операцию, идеально подходят для перемещения, растяжения, масштабирования и вращения визуальных объектов.

В некоторых приложениях выбор между событиями Pointer и Manipulation не столь очевиден. Вероятно, первым кандидатом все же должны стать события Manipulation — особенно если вы думаете «Надеюсь, пользователю не придет в голову использовать второй палец, потому что я его все равно игнорирую». Если пользователь использует два пальца там, где достаточно одного, ввод с нескольких пальцев будет объединен.

Однако события Manipulation связаны с неизбежной задержкой. Палец, прикасающийся к экрану, должен слегка переместиться, чтобы его действие было интерпретировано как часть операции. События Manipulation не срабатывают при касании или простом нажатии. Иногда этой задержки бывает достаточно для перехода на события Pointer. Примером служит пользовательский элемент управления XYSlider, который мы создадим позже. В приведенной версии используются события Manipulation, потому что действия нескольких пальцев в этом контексте не нужны. Но время задержки представляет бесспорную проблему, поэтому позже будет приведена другая версия, использующая события Pointer.

События Pointer генерируются на уровне окна объектом CoreWindow, и вы можете определять собственные события Manipulation с использованием GestureRecognizer, но я ограничусь событиями, определяемыми UIElement, и виртуальными методами, определяемыми Control. Также мы не будем подробно рассматривать информацию об устройствах, которую можно получить из классов пространства имен Windows.Devices.Input.

События Pointer

Из восьми событий Pointer пять встречаются очень часто. Если прикоснуться пальцем к доступному и видимому экземпляру класса, производного от UIElement, переместить и поднять палец, будут сгенерированы пять событий Pointer в следующей последовательности:

PointerEntered
PointerPressed
PointerMoved (в общем случае несколько событий)
PointerReleased
PointerExited

События Pointer от пальцев генерируются только при прикосновении к экрану или отпускании. В области сенсорного ввода не существует такого понятия, как «наведение» (hover). С мышью дело обстоит иначе. Мышь генерирует события PointerHoved даже без нажатия кнопки. Предположим, вы подвели указатель мыши к конкретному элементу, нажали кнопку, переместили мышь, отпустили кнопку, а затем сдвинули мышь за границы элемента. Элемент генерирует следующую серию событии:

PointerEntered
PointerHoved (несколько событий)
PointerPressed
PointerMoved (несколько событий)
PointerReleased
PointerMoved (несколько событий)
PointerExited

Множественные события PointerPressed и PointerReleased также могут генерироваться при нажатии и отпускании разных кнопок мыши.

Теперь займемся пером. Элемент начинает реагировать на перо еще до того, как пользователь прикоснется к экрану, поэтому сначала происходит событие PointerEntered, а за ним следует событие PointerMoved. При прикосновении к экрану генерируется событие PointerPressed. Переместите перо и поднимите его. Элемент продолжает выдавать события PointerMoved после PointerReleased, но при дальнейшем перемещении пера серия завершается событием PointerExited. В итоге генерируется та же последовательность событий, что и с мышью.

Когда пользователь поворачивает колесо мыши, генерируется событие PointerWheelChanged.

Два оставшихся события встречаются еще реже: PointerCaptureLost и PointerCanceled. Захват указателя будет рассматриваться позже, когда важность события PointerCaptureLost станет более очевидной.

Я еще никогда не видел события PointerCanceled — даже при отключении мыши от компьютера, но событие существует для передачи информации о таких ошибках.

Ко всем перечисленным событиям прилагается экземпляр класса PointerRoutedEventArgs, определенного в пространстве имен Windows.UI.Xaml.Input. (Будьте внимательны: в пространстве имен Windows.UI.Core также присутствует класс PointerEventArgs, но он используется для обработки ввода от указателя на уровне окна.) Как подсказывает имя класса, все эти события Pointer относятся к категории перенаправляемых (routed) событий, передаваемых по визуальному дереву.

Класс PointerRoutedEventArgs определяет два свойства, общих для перенаправляемых событий:

  1. Свойство OriginalSource обозначает элемент, инициировавший событие.

  2. Свойство Handled позволяет остановить дальнейшее перенаправление события но визуальному дереву.

Объект PointerRoutedEventArgs содержит много полезной информации; ниже выделены лишь важнейшие моменты. Класс также определяет следующие члены:

Будьте внимательны: мы уже начинаем работать с классами Pointer (определяемым в пространстве имен Windows.UI.Xaml.Input) и PointerPoint (определяемым в пространстве имен Windows.UI.Input).

Класс Pointer содержит всего четыре свойства:

Свойство PointerId

Содержит целочисленный идентификатор, определяющий мышь, перо или отдельный палец.

Свойство PointerDeviceType

Принимает значения Touch, Mouse или Pen.

Логическое свойство IsInRange

Определяет, находится ли устройство в диапазоне экрана.

Логическое свойство IsInContact

Определяет, прикасается ли палец или перо к экрану, или нажата ли кнопка мыши.

Свойство PointerId играет исключительно важную роль — оно используется для отслеживания движений отдельных пальцев. Программа, обрабатывающая события Pointer, почти всегда определяет словарь, в котором свойство PointerId служит ключом.

Судя по имени, метод GetCurrentPoint() класса PointerRoutedEventArgs возвращает текущие координаты указателя — и это действительно так, но кроме этого, метод также возвращает много другой полезной информации. Так как позицию удобно отсчитывать относительно конкретного элемента, метод GetCurrentPoint() получает аргумент типа UIElement. Объект PointerPoint, возвращаемый этим методом, дублирует часть информации из Pointer (свойства PointerId и IsInContact), а также предоставляет другую полезную информацию:

Position

Тип Point, возвращает координаты (x, y) указателя на момент события.

Timestamp

Тип ulong, временная метка.

Properties

Тип PointerPointProperties, определяется в Windows.UI.Input — дополнительная информация.

Свойство Position всегда задается относительно левого верхнего угла элемента, передаваемого методу GetCurrentPoint().

Класс PointerRoutedEventArgs также определяет метод с именем GetIntermediatePoints(), который похож на GetCurrentPoint(), но возвращает коллекцию объектов PointerPoint. Очень часто эта коллекция содержит всего один объект (тот же объект PointerPoint, который возвращает GetCurrentPoint), но для события PointerMoved таких объектов может быть несколько, особенно если обработчик события работает не слишком быстро. В частности, я заметил, что GetIntermediatePoints возвращает несколько объектов PointerPoint на планшете Microsoft Surface.

Класс PointerPointProperties определяет 22 свойства, предоставляющих подробную информацию о событии: признаки нажатия кнопок мыши, признак нажатия кнопки на корпусе пера, наклон пера, контактный прямоугольник пальца на экране (если информация доступна), сила нажатия пальца или пера (если информация доступна) и MouseWheelDelta. Вы можете использовать всю эту информацию по своему усмотрению. Разумеется некоторые свойства будут недоступны на некоторых устройствах, а следовательно, сохранят значения по умолчанию.

Простое сенсорное приложение

Вероятно, классическим мультисенсорным приложением является программа, которая позволяет рисовать пальцами на экране. Чтобы написать такую программу, достаточно обрабатывать всего три события Pointer и проверять два свойства из аргументов события, но результат имеет серьезный недостаток, который не компенсируется его простотой.

Файл MainPage.xaml проекта FingerPaint1 просто назначает имя для стандартной панели Grid:

<Page ...>

    <Grid Name="grid" Background="#FF1D1D1D">
        
    </Grid>
</Page>

Прежде всего файл фонового кода определяет объект Dictionary с ключом типа uint. Ранее я упоминал о том, что практически каждая программа, обрабатывающая события Pointer, содержит некую разновидность Dictionary. Тип данных, хранящихся в словаре, зависит от приложения; иногда приложение определяет класс или структуру специально для этой цели. В простейшей программе для рисования каждый палец, прикасающийся к экрану, будет рисовать свой объект Polyline, так что в Dictionary может храниться этот экземпляр Polyline:

using System;
using System.Collections.Generic;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        Dictionary<uint, Polyline> pointerDic = new Dictionary<uint, Polyline>();
        Random random = new Random();
        byte[] rgb = new byte[3];

        public MainPage()
        {
            InitializeComponent();
        }

        protected override void OnPointerPressed(PointerRoutedEventArgs e)
        {
            // Получение информации из аргументов события
            uint id = e.Pointer.PointerId;
            Point point = e.GetCurrentPoint(this).Position;

            // Создание случайного цвета
            random.NextBytes(rgb);
            Color color = Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);

            // Создание объекта Polyline
            Polyline polyline = new Polyline
            {
                Stroke = new SolidColorBrush(color),
                StrokeThickness = 24,
            };
            polyline.Points.Add(point);

            // Добавление на панель Grid
            grid.Children.Add(polyline);

            // Включить в словарь
            pointerDic.Add(id, polyline);
            base.OnPointerPressed(e);
        }

        protected override void OnPointerMoved(PointerRoutedEventArgs e)
        {
            // Получение информации из аргументов события
            uint id = e.Pointer.PointerId;
            Point point = e.GetCurrentPoint(this).Position;

            // Если идентификатор хранится в словаре, 
            // добавить точку в Polyline
            if (pointerDic.ContainsKey(id))
                pointerDic[id].Points.Add(point);

            base.OnPointerMoved(e);
        }

        protected override void OnPointerReleased(PointerRoutedEventArgs e)
        {
            // Получение информации из аргументов события
            uint id = e.Pointer.PointerId;

            // Если идентификатор хранится в словаре, удалить его
            if (pointerDic.ContainsKey(id))
                pointerDic.Remove(id);

            base.OnPointerReleased(e);
        }
    }
}

В переопределении OnPointerPressed программа создает объект Polyline и назначает ему случайный цвет. Первая точка определяется по местонахождению указателя. Объект Polyline добавляется на панель Grid и в словарь.

При последующих вызовах OnPointerMoved свойство PointerId идентифицирует палец, так что программа может обратиться к объекту Polyline, связанному с этим пальцем, и включить новый объект Point в Polyline. Так как это тот же экземпляр, что и Polyline в Grid, длина экранного объекта увеличивается по мере движения пальца.

Обработчик OnPointerReleased просто удаляет объект Polyline из словаря, считая его завершенным.

Запустите программу и проведите несколькими пальцами по экрану:

Сенсорное рисование в Windows Runtime

Каждый палец рисует собственную линию как серию соединенных точек определенного цвета. Для рисования можно использовать мышь и перо.

Ранее я упоминал о том, что у этого кода есть недостаток. Переопределения OnPointerMoved и OnPointerReleased проверяют, что идентификатор существует как ключ в словаре, прежде чем использовать его для обращения к словарю. Это очень важно для обработки операций с мышью и пером, потому что эти устройства генерируют события PointerMoved до событий OnPointerPressed. Но попробуйте сделать следующее: переключите программу в режим Snap View, пальцем проведите линию за пределы страницы, а потом обратно.

Ограничение рисования в приложении Windows Runtime, находящемся в режиме Snap View

Обратите внимание на прямую линию на левой стороне. Она рисуется, когда палец снова входит на страницу, и указывает на то, что программа не получает события PointerMoved в то время, когда палец выходит за ее пределы. Повторите опыт с мышью — то же самое.

А теперь попробуйте сделать следующее: проведите пальцем линию изнутри страницы в наружную точку и поднимите палец. Затем снова проведите внутри страницы. Вроде бы все нормально.

Теперь попробуйте сделать то же с мышью. Нажмите кнопку мыши над страницей FingerPaint1, переместите мышь за пределы страницы и отпустите кнопку. Теперь снова переведите мышь на страницу FingerPaint1. Программа продолжает рисовать линию, несмотря на то, что кнопка мыши отпущена! Это безусловно неправильно (хотя вам наверняка встречались программы, которые ведут себя подобным образом). Теперь при нажатии кнопки мыши выдается исключение, потому что метод OnPointerPressed пытается добавить в словарь объект с ключом, уже существующим в словаре. В отличие от пера или касаний пальцем, все события мыши имеют одинаковые идентификаторы. Давайте исправим эти недостатки в следующей статье.

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