Использование пера в WinRT

147

Темой этой и последующих статей является перо, или стилус — устройство ввода, будущее которого в компьютерных технологиях выглядит довольно неопределенно и у которого имеются как убежденные сторонники, так и убежденные противники. В 2010 году Стив Джобс, обсуждая возможности конкуренции с iPad со стороны других планшетных устройств, заявил: «Если вы видите стилус — значит, они облажались».

Тем не менее каждый, кто пытался использовать традиционное приложение с мышью на сенсорном экране, наверняка не согласится с этим мнением. Стилус не так удобен, как палец, и не так универсален, как несколько пальцев, но по точности он почти не уступает мыши, а для выбора команд меню, выделения и стирания ввода он подходит намного лучше пальцев. Планшет Samsung 700T, который я использовал для многих программ, оснащен стилусом.

Лично я предпочитаю называть эти устройства ввода «стилусами», но далее будет использоваться терминология, соответствующая интерфейсу программирования Windows Runtime, где это устройство называется «пером» (pen).

Вы уже знаете, как обрабатывать и отображать на экране ввод с пера, однако пространство имен Windows.UI.Input.Inking предоставляет дополнительные функции при работе с пером, в числе которых:

Интересно, что ни одна из этих функций не требует непременного наличия пера! Теоретически все, о чем говорится в этой и последующих статьях, можно сделать вводом с мыши или сенсорного экрана. Впрочем, рукописный ввод с сенсорного экрана или мыши неудобен, потому что нарисованный пальцами текст обычно оказывается слишком крупным, а текст, нарисованный мышью, — слишком неровным. Перо идеально подходит для этой цели — как и следовало ожидать от устройства, размер и форма которого использовались для письма не менее двух тысячелетий.

Гораздо более универсальным устройством является электромагнитное перо, иногда называемое дигитайзером или цифровым стилусом, но для таких устройств необходим экран, реагирующий на перьевой ввод такого рода — как у планшета Samsung 700T У такого пера имеется небольшой наконечник (около 1 мм в диаметре), «ластик» на другом конце и кнопка на стержне. Класс PointerPointProperties определяет два свойства IsEraser и IsInverted; оба свойства истинны, если пользователь прикасается к экрану «ластиком», а не наконечником пера. Чаще всего эта возможность используется для стирания предыдущего ввода. Свойство IsBarrelButtonPressed истинно, если пользователь касается экрана наконечником, нажимая кнопку на пере. Обычно данная функция используется для выделения.

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

Коллекции InkManager

В пространстве имен Windows.UI.Input.Inking центральное место занимает класс InkManager. Он открывает приложению доступ ко многим функциям, относящимся к использованию пера.

Экземпляр InkManager управляет всем перьевым вводом для конкретной страницы ввода. Если ваша программа реализует некое подобие «рисовального блокнота», каждая страница этого блокнота будет использовать собственный экземпляр InkManager.

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

Каждый объект InkStroke содержит коллекцию объектов InkStrokeRenderingSegment. Объект InkStrokeRenderingSegment представляет отдельную кривую Безье с конкретной силой нажатия, наклоном и изгибом. Сила нажатия часто используется для определения толщины линии при выводе штрихов. Значение может изменяться в диапазоне от 0 до 1, как и свойство Pressure объекта PointerPointProperties. Перья, поддерживающие наклон и изгиб, встречаются относительно редко.

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

  1. Рисование линий с использованием объектов Polyline, Line или Path в процессе того, как пользователь рисует или пишет пером.

  2. Замена этих элементов кривыми Безье при завершении каждого штриха.

Первый шаг уже рассматривался в контексте программ FingerPaint. Чтобы основные принципы использования InkManager стали более понятными, в первом примере SimpleInking мы сосредоточимся на втором шаге. Программа SimpleInking настолько проста, что рисунок остается невидимым до того момента, когда перо отрывается от экрана!

Ниже приведен файл XAML. Панель Grid окрашена в белый цвет, как это принято для перьевого ввода:

<Page ...>

    <Grid Name="contentGrid" Background="#FFF" />
</Page>

По умолчанию для перьевого ввода используется черный цвет. Я использовал один объект InkManager для всей программы:

using System.Linq;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Input;
using Windows.Devices.Input;
using Windows.UI.Input;
using System.Collections.Generic;
using Windows.UI.Input.Inking;
using Windows.UI.Xaml.Shapes;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        InkManager inkManager = new InkManager();
        bool hasPen;

        public MainPage()
        {
            this.InitializeComponent();

            // Проверить, входит ли перо в число устройств ввода
            foreach (PointerDevice device in PointerDevice.GetPointerDevices())
                hasPen |= device.PointerDeviceType == PointerDeviceType.Pen;
        }
        
        // ...
    }
}

Конструктор определяет, поддерживает ли машина перо, и если поддерживает - задает свойству hasPen значение true. В этой программе я решил игнорировать на компьютерах с поддержкой пера весь ввод, кроме перьевого, но на компьютерах без поддержки пера весь ввод с указателя принимается. Это позволяет использовать данную программу на устройствах Microsoft Surface.

Класс InkManager определяет три метода, используемых в сочетании с событиями Pointer. Эти методы позволяют объекту InkManager накапливать ввод с указателя:

В аргументе каждого метода передается объект PointerPoint, который можно получить из PointerRoutedEventArgs вызовом GetCurrentPoint или GetIntermediatePoints. Напомню, что объект PointerPoint включает не только позицию указателя, но и его идентификатор (что позволяет InkManager отслеживать несколько указателей) и объект PointerPointProperties с информацией о силе нажатия и наклоне.

Вот как выглядит переопределение OnPointerPressed в приложении SimpleInking:

protected override void OnPointerPressed(PointerRoutedEventArgs e)
{
    if (e.Pointer.PointerDeviceType == PointerDeviceType.Pen || !hasPen)
    {
        PointerPoint pointerPoint = e.GetCurrentPoint(this);
        inkManager.ProcessPointerDown(pointerPoint);
    }
    base.OnPointerPressed(e);
}

Команда if проверяет, что устройство имеет тип Pen, но разрешает использовать другие устройства ввода с указателя, если компьютер не поддерживает перо. Чтобы программа поддерживала все устройства ввода с указателя независимо от типа, достаточно убрать всю команду if. В любом случае просто передайте объект PointerPoint методу ProcessPointerDown объекта InkManager.

Обработка события OnPointerMoved организована чуть сложнее:

protected override void OnPointerMoved(PointerRoutedEventArgs e)
{
    if ((e.Pointer.PointerDeviceType == PointerDeviceType.Pen || !hasPen) &&
        e.Pointer.IsInContact)
    {
        IEnumerable<PointerPoint> points = e.GetIntermediatePoints(this).Reverse();

        foreach (PointerPoint point in points)
            inkManager.ProcessPointerUpdate(point);
    }
    base.OnPointerMoved(e);
}

Вызовы ProcessPointerUpdate позволяют объекту InkManager накопить части итогового штриха. Для достижения максимальной точности ввода в программе используется вызов GetIntermediatePoints вместо GetCurrentPoint, инвертированный оператором LINQ Reverse.

Также обратите внимание на то, что команда if для OnPointerMoved теперь включает проверку свойства IsInContact. Перо начинает генерировать события OnPointerMoved тогда, когда оно оказывается вблизи экрана, но до непосредственного соприкосновения с ним. Если команда if не будет проверять IsInContact, то метод ProcessPointerUpdate объекта InkManager будет вызван раньше вызова ProcessPointerDown, а это приведет к выдаче исключения.

До сих пор наша программа ничего не рисует. Все рисование выполняется в методе OnPointerReleased, но сначала давайте рассмотрим служебную информацию InkManager:

protected override void OnPointerReleased(PointerRoutedEventArgs e)
{
    if (e.Pointer.PointerDeviceType != PointerDeviceType.Pen && hasPen)
        return;

    inkManager.ProcessPointerUp(e.GetCurrentPoint(this));

    // Прорисовка самого нового объекта InkStroke
    //...
            
    base.OnPointerReleased(e);
}

Прежде чем выводить на экран новый объект InkStroke, необходимо познакомиться с классом InkDrawingAttributes.

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