Атрибуты рисования пером в WinRT

124

Хотя класс InkManager сам по себе не выполняет вывода, он хранит информацию о связанных с ним атрибутах. Эта информация инкапсулируется в классе InkDrawingAttributes. Свойства и значения по умолчанию этого класса представлены в следующей таблице.

Свойства класса InkDrawingAttributes
Свойство Значение по умолчанию
Color

Black (0xFF000000)

PenTip

Circle

Size

(2,2)

FitToCurve

true

IgnorePressure

false

Значения по умолчанию используются при «ручном» создании нового экземпляра InkDrawingAttributes, и они же используются объектами InkDrawingAttributes, которые InkManager создает и хранит во внутренней реализации. Эти свойства предназначены исключительно для приложений, которые выводят на экран результаты операций с пером! Они никак не влияют на работу InkManager, потому что InkManager не занимается графическим выводом.

Свойство PenTip также может принимать значение Rectangle; в этом случае размеры прямоугольного наконечника пера описываются свойством Size (типа Size). Для используемого по умолчанию круглого наконечника (Circle) толщина линий может определяться свойством Width значения Size.

Свойство FitToCurve указывает, должен ли рисунок выводиться в формате кривых Безье; независимо от его значения InkManager преобразует ввод с указателя в кривые Безье. Свойство IgnorePressure указывает, что при выводе информация о силе нажатия не учитывается, но InkManager включает информацию силы нажатия независимо от значения.

InkManager создает объект InkDrawingAttributes с этими значениями по умолчанию и хранит его в своей внутренней реализации. Тем не менее для вашей программы этот объект недоступен. Если вам потребуется задать различные свойства объекта InkManager, это придется делать так:

InkDrawingAttributes inkDAttrs = new InkDrawingAttributes();
inkDAttrs.Color = Colors.Green;
inkDAttrs.Size = new Size(6, 6);
inkManager.SetDefaultDrawingAttributes(inkDAttrs);

Когда вы создаете новый объект InkDrawingAttributes и передаете его InkManager, не следует полагать, что этот объект отныне совместно используется вашей программой и InkManager и все изменения, внесенные в приложении, будут отражены во внутреннем объекте, хранимом InkManager. Если ваша программа вносит изменения в объект InkDrawingAttributes, она должна повторно вызвать метод SetDefaultDrawingAttributes, чтобы они вступили в силу.

Как видите, программа, использующая InkManager, обрабатывает нормальную последовательность событий из PointerPressed, нескольких событий PointerMoved и PointerReleased, вызовами методов InkManager ProcessPointerDown, ProcessPointerUpdate (многократно) и ProcessPointerUp. При завершении этой последовательности вызовов InkManager создает новый объект InkStroke и добавляет его в коллекцию.

Объект InkStroke представляет непрерывный штрих, проведенный от момента прикосновения указателя к экрану до момента потери контакта. Объект InkStroke содержит свойство DrawingAttributes типа InkDrawingAttributes, который создается объектом InkManager на основании внутреннего объекта InkDrawingAttributes со значениями по умолчанию.

Например, допустим, что вы создали новый объект InkManager и обработали ввод с указателя вызовами ProcessPointerDown, ProcessPointerUpdate (многократно) и ProcessPointerUp. Полученный объект InkStroke содержит объект InkDrawingAttributes с обозначением черного пера с размером (2, 2). Затем ваша программа создает новый объект InkDrawingAttributes, задает пару свойств и вызывает SetDefaultDrawingAttributes с использованием кода, приведенного ранее. Следующая последовательность вызовов ProcessPointerDown, ProcessPointerUpdate и ProcessPointerUp приводит к созданию второго объекта InkStroke, но у этого объекта свойство DrawingAttributes обозначает красное перо с размером (6, 6).

Но не стоит полагать, что атрибуты задаются раз и навсегда. Вы можете создавать новые объекты InkDrawingAttributes и связывать их с конкретными объектами InkStroke, а также изменять любые свойства существующих объектов InkDrawingAttributes, на которые ссылается свойство DrawingAttributes объекта InkStroke.

После вызова метода ProcessPointerUp объект InkManager преобразует все точки, накопленные для нового штриха во внутренней реализации, в одну или несколько кривых Безье, которые составляют новый объект InkStroke. Этот новый объект InkStroke добавляется во внутреннюю коллекцию, к которой можно обратиться при помощи метода GetStrokes.

Так как программа SimpleInking выводит каждый штрих после его завершения, для нее представляет интерес только самый последний объект InkStroke в коллекции, который она получает следующим образом:

IReadOnlyList<InkStroke> inkStrokes = inkManager.GetStrokes();
InkStroke inkStroke = inkStrokes[inkStrokes.Count - 1];

Объект InkStroke содержит свойство DrawingAttributes и коллекцию объектов InkStrokeRenderingSegment, представляющую серию соединенных кривых Безье. Программа может получить коллекцию сегментов вызовом GetRenderingSegments. Каждый объект InkStrokeRenderingSegment содержит три свойства типа Point:

BezierControlPoint1
BezierControlPoint2
Position

У первого объекта InkStrokeRenderingSegment в коллекции эти три точки совпадают. Это первая точка полной кривой; каждый последующий объект InkStrokeRenderingSegment продолжает формирование кривой, дополняя ее двумя контрольными точками и конечной точкой.

Кроме того, каждый объект InkStrokeRenderingSegment также содержит четыре свойства типа float:

Pressure
TiltX
TiltY
Twist

Конечно, они предназначены для более сложных систем перьевого ввода! На планшете Samsung 700T значения Pressure изменяются в диапазоне от 0 до 1, но три других свойства имеют значение по умолчанию 0,5. Теоретически свойства TiltX и TiltY определяют наклон пера относительно экрана; свойство Twist относится только к прямоугольным наконечникам и определяет степень поворота наконечника относительно оси экрана.

В примерах я буду учитывать значение Pressure. Как вы помните по приложениям FingerPaint, объект Polyline хорошо подходил для вывода связной кривой в том случае, если сила нажатия игнорировалась. Если же она учитывается, для имитации прямой линии с переменной толщиной приходится использовать либо несколько отдельных элементов Line с разной толщиной линий, либо несколько отдельных элементов Path.

Код приложения SimpleInking отображает каждый объект InkStrokeRenderingSegment (кроме первого) как элемент Path, у которого PathGeometry состоит из одного объекта PathFigure с одним объектом BezierSegment. Свойство Stroke этого элемента Path содержит объект SolidColorBrush, созданный по значению свойства Color свойства DrawingAttributes объекта InkStroke. Свойство StrokeThickness определяется произведением свойства Size объекта DrawingAttributes для InkStroke и свойства Pressure конкретного сегмента InkStrokeRenderingSegment:

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

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

    // Вывод последнего объекта InkStroke
    IReadOnlyList<InkStroke> inkStrokes = inkManager.GetStrokes();
    InkStroke inkStroke = inkStrokes[inkStrokes.Count - 1];

    // Создание объекта SolidColorBrush для всех сегментов штриха
    Brush brush = new SolidColorBrush(inkStroke.DrawingAttributes.Color);

    // Получение сегментов
    IReadOnlyList<InkStrokeRenderingSegment> inkSegments = inkStroke.GetRenderingSegments();

    // Обратите внимание: цикл начинается с 1
    for (int i = 1; i < inkSegments.Count; i++)
    {
        InkStrokeRenderingSegment inkSegment = inkSegments[i];

        // Создание BezierSegment по точкам
        BezierSegment bezierSegment = new BezierSegment
        {
            Point1 = inkSegment.BezierControlPoint1,
            Point2 = inkSegment.BezierControlPoint2,
            Point3 = inkSegment.Position
        };

        // Создание объекта PathFigure, начинающегося с предыдущей позиции
        PathFigure pathFigure = new PathFigure
        {
            StartPoint = inkSegments[i - 1].Position,
            IsClosed = false,
            IsFilled = false
        };
        pathFigure.Segments.Add(bezierSegment);

        // Создание объекта PathGeometry для получения объекта PathFigure
        PathGeometry pathGeometry = new PathGeometry();
        pathGeometry.Figures.Add(pathFigure);

        // Создание объекта Path для получения объекта PathGeometry
        Path path = new Path
        {
            Stroke = brush,
            StrokeThickness = inkStroke.DrawingAttributes.Size.Width *
              inkSegment.Pressure,
            StrokeStartLineCap = PenLineCap.Round,
            StrokeEndLineCap = PenLineCap.Round,
            Data = pathGeometry
        };

        // Добавление на панель Grid
        contentGrid.Children.Add(path);
    }

    base.OnPointerReleased(e);
}

Цикл for в коллекции объектов InkStrokeRenderingSegment начинается с 1, потому что первый объект представляет только начальную точку, а каждый последующий объект InkStrokeRenderingSegment — кривую Безье. В каждом объекте PathGeometry свойство StartPoint объекта PathFigure содержит свойство Position из предыдущего объекта InkStrokeRenderingSegment. Хотя я и не видел, что у меня получается во время рисования, мне удалось изобразить следующее сообщение:

Сенсорный ввод пером в приложении Windows Runtime

Класс InkManager преобразует ломаную в серию кривых Безье не только для получения более плавного вывода, но и для сокращения объема данных. Даже если учесть, что каждый сегмент ломаной представляется одной точкой, а каждый сегмент кривой Безье (после первого) — тремя точками, сокращение объема данных все равно получается значительным.

Если вы предпочитаете игнорировать силу нажатия, можно использовать для всего объекта InkStroke один элемент Path, определить геометрию одним элементом PolyBezierSegment, заполнить коллекцию Points точками из каждого объекта InkStrokeRenderingSegment, используя первый только для задания свойства StartPoint объекта PathFigure. Это альтернативное решение выглядит так:

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

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

    // Вывод последнего объекта InkStroke
    IReadOnlyList<InkStroke> inkStrokes = inkManager.GetStrokes();
    InkStroke inkStroke = inkStrokes[inkStrokes.Count - 1];
    
    // Создание объекта PolyBezierSegment для всех сегментов штриха
    IReadOnlyList<InkStrokeRenderingSegment> inkSegments = 
        inkStroke.GetRenderingSegments();

    PolyBezierSegment polyS = new PolyBezierSegment();
    for (int i = 1; i < inkSegments.Count; i++)
    {
        InkStrokeRenderingSegment inkSegment = inkSegments[i];

        polyS.Points.Add(inkSegment.BezierControlPoint1);
        polyS.Points.Add(inkSegment.BezierControlPoint2);
        polyS.Points.Add(inkSegment.Position);
    }

    // Создание объекта PathFigure, начинающегося с первой точки
    PathFigure pathFigure = new PathFigure
    {
        StartPoint = inkSegments[0].Position,
        IsClosed = false,
        IsFilled = false
    };
    pathFigure.Segments.Add(polyS);

    // Создание объекта PathGeometry для получения объекта PathFigure
    PathGeometry pathGeometry = new PathGeometry();
    pathGeometry.Figures.Add(pathFigure);

    // Создание объекта Path для получения объекта PathGeometry
    Path path = new Path
    {
        Stroke = new SolidColorBrush(inkStroke.DrawingAttributes.Color),
        StrokeThickness = inkStroke.DrawingAttributes.Size.Width,
        StrokeStartLineCap = PenLineCap.Round,
        StrokeEndLineCap = PenLineCap.Round,
        Data = pathGeometry
    };

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

    base.OnPointerReleased(e);
}

Легко убедиться в том, что InkManager не пытается захватывать указатель - вам придется реализовать захват самостоятельно. Впрочем, если во время накопления ввода перо выходит за пределы элемента управления и вы отрываете его от экрана, объект InkManager корректно продолжает работу. Он не выдает исключение при получении очередного вызова ProcessPointerDown, если предыдущая серия не была завершена вызовом ProcessPointerUp.

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