Стирания и другие манипуляции в WinRT

80

Одним из очевидных усовершенствований программы SimpleInking является вывод ломаной во время рисования пером (пожалуй, это правильнее считать минимальным требованием, а не усовершенствованием!) Вероятно, ломаная будет изображаться либо набором элементов Line или Path, если сила нажатия учитывается, либо элементом Polyline, если она игнорируется. Но после реализации этой логики появляется выбор: то ли заменить ломаную кривыми Безье после завершения штриха, то ли оставить ее на экране.

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

Когда вы начнете размышлять над этой проблемой, становится очевидно, что эти кривые Безье не должны иметь одинаковую толщину линии. Например, если штрих состоит из четырех объектов InkStrokeRenderingSegment со значениями Pressure 0.25, 0.5, 0.6 и 0.4, у первой кривой Безье толщина должна увеличиваться от 0.25 до 0.5, у второй — также увеличиваться от 0.5 до 0.6, и у последней — уменьшаться от 0.6 до 0.4. Это означает, что вы не можете просто задать свойства BezierSegment из объектов InkStrokeRenderingSegment. Вероятно, следует синтезировать контур штриха по точкам кривых Безье и значениям силы нажатия, а затем использовать объект Path для его заполнения, как это делалось в программе FingerPaint. Но разумеется, для кривых Безье задача обладает куда большей алгоритмической сложностью, чем для отрезков прямых.

Другая проблема: программа SimpleInking выводит каждый новый штрих при его завершении и добавляет элементы Path на панель Grid с именем contentGrid. Если вы рисуете ломаные в процессе создания штриха, а затем заменяете их кривыми Безье, более ранние элементы необходимо удалить с Grid. А если в вашем приложении будет реализовано стирание, в какой-то момент вам, вероятно, потребуется удалить кривые Безье с Grid, но для этого их нужно будет как-то пометить.

Эти две проблемы наводят на мысль, что будет проще очищать коллекцию Children панели Grid в обработчике OnPointerReleased, а затем рисовать все «с нуля». Обычно это решение необходимо при стирании, но действовать так постоянно не обязательно, особенно если вы определяете разные элементы Grid для рисования предварительных линий и окончательных кривых Безье.

Класс InkManager содержит свойство Mode типа InkManipulationMode — перечисления с тремя значениями:

Inking
Erasing
Selecting

По умолчанию используется значение Inking. Чтобы включить режим стирания, задайте свойству значение Erasing в обработчике OnPointerPressed и продолжайте обычную обработку, но без какого-либо вывода. Затем при последующих вызовах метода ProcessPointerUpdate класса InkManager, если перемещение пера пересекает существующий штрих, InkManager удаляет этот штрих из своей коллекции.

Хотя вы можете заново вывести все оставшиеся объекты InkStroke в обработчике OnPointerReleased, сам штрих удаляется в обработчике OnPointerMoved. Чтобы сообщить пользователю о том, что штрих был удален, не обязательно дожидаться OnPointerReleased.

Новый проект называется InkAndErase. Чтобы упростить удаление предварительных элементов Line, созданных в процессе рисования нового штриха, в файл XAML включаются два элемента Grid:

<Page ...>

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

Панель contentGrid предназначена для законченных штрихов, выводимых в виде кривых Безье, тогда как панель newLineGrid предназначена для элементов Line, создаваемых в процессе создания штриха. Такое разделение позволяет легко избавиться от сегментов Line простой очисткой коллекции Children панели newLineGrid.

Файл фонового кода создает объект InkDrawingAttributes и задает его объекту InkManager со значениями, отличными от значений по умолчанию (ради разнообразия); при этом объект InkDrawingAttributes также хранится в поле для кода рисования линий в переопределении OnPointerMoved. Поскольку эта программа выполняет собственную обработку ввода с указателя, она определяет объект Dictionary для хранения информации, относящейся к каждому указателю:

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

    Dictionary<uint, Point> pointerDictionary = new Dictionary<uint, Point>();

    public MainPage()
    {
        this.InitializeComponent();

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

        // Атрибуты вывода по умолчанию
        inkDrawingAttributes.Color = Colors.Blue;
        inkDrawingAttributes.Size = new Size(6, 6);
        inkManager.SetDefaultDrawingAttributes(inkDrawingAttributes);
    }
        
        // ...
        
}

Код вывода кривых Безье в приложении InkAndErase практически идентичен коду предыдущей программы, но я разделил его на два метода, чтобы можно было перерисовать как весь рисунок, так и отдельные объекты InkStroke:

public sealed partial class MainPage : Page
{

    // ...
    
    private void RenderAll()
    {
        contentGrid.Children.Clear();

        foreach (InkStroke inkStroke in inkManager.GetStrokes())
        RenderStroke(inkStroke);
    }

    private void RenderStroke(InkStroke inkStroke)
    {
        Brush brush = new SolidColorBrush(inkStroke.DrawingAttributes.Color);
        IReadOnlyList<InkStrokeRenderingSegment> inkSegments = inkStroke.GetRenderingSegments();

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

        BezierSegment bezierSegment = new BezierSegment
        {
            Point1 = inkSegment.BezierControlPoint1,
            Point2 = inkSegment.BezierControlPoint2,
            Point3 = inkSegment.Position
        };

        PathFigure pathFigure = new PathFigure
        {
            StartPoint = inkSegments[i - 1].Position,
            IsClosed = false,
            IsFilled = false
        };
        pathFigure.Segments.Add(bezierSegment);

        PathGeometry pathGeometry = new PathGeometry();
        pathGeometry.Figures.Add(pathFigure);

        Path path = new Path
        {
            Stroke = brush,
            StrokeThickness = inkStroke.DrawingAttributes.Size.Width * 
                      inkSegment.Pressure,
            StrokeStartLineCap = PenLineCap.Round,
            StrokeEndLineCap = PenLineCap.Round,
            Data = pathGeometry
        };
        contentGrid.Children.Add(path);
        }
    }
}

Кроме взаимодействия с объектом InkManager, большая часть кода обработки событий Pointer очень похожа на код из программы FingerPaint4, в которой обрабатывалась сила нажатия. Переопределение OnPointerPressed единственное место, в котором программа проверяет, что устройством ввода с указателя является перо. Следующие переопределения Pointer используют присутствие ключа с идентификатором указателя в словаре pointerDictionary для определения того, что в настоящий момент выполняется операция рисования.

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

protected override void OnPointerPressed(PointerRoutedEventArgs e)
{
    if (e.Pointer.PointerDeviceType == PointerDeviceType.Pen || !hasPen)
    {
        // Получение информации
        PointerPoint pointerPoint = e.GetCurrentPoint(this);
        uint id = pointerPoint.PointerId;

        // Инициализация для рисования или стирания
        if (!pointerPoint.Properties.IsEraser)
        {
            inkManager.Mode = InkManipulationMode.Inking;
        }
        else
        {
            inkManager.Mode = InkManipulationMode.Erasing;
        }

        // Передача PointerPoint объекту InkManager
        inkManager.ProcessPointerDown(pointerPoint);

        // Добавление пары в словарь
        pointerDictionary.Add(e.Pointer.PointerId, pointerPoint.Position);

        // Захват указателя
        CapturePointer(e.Pointer);
    }
    base.OnPointerPressed(e);
}

Переопределение OnPointerPressed завершается захватом указателя. Переопределение OnPointerMoved создает и рисует элемент Line, как в программе FingerPaint4, но только в том случае, если программа не работает в режиме стирания. При стирании проверяется значение, возвращаемое ProcessPointerUpdate. В случае удаления штриха из коллекции оно представляет собой непустой объект Rect, обозначающий перерисовываемую область экрана. Метод реагирует перерисовкой всей коллекции штрихов, в которой теперь нет удаленного штриха:

protected override void OnPointerMoved(PointerRoutedEventArgs e)
{
    // Получение информации
    PointerPoint pointerPoint = e.GetCurrentPoint(this);
    uint id = pointerPoint.PointerId;

    if (pointerDictionary.ContainsKey(id))
    {
        foreach (PointerPoint point in e.GetIntermediatePoints(this).Reverse())
        {
            // Передача PointerPoint объекту InkManager
            object obj = inkManager.ProcessPointerUpdate(point);

            if (inkManager.Mode == InkManipulationMode.Erasing)
            {
                // Проверить, было ли что-то удалено
                Rect rect = (Rect)obj;

                if (rect.Width != 0 && rect.Height != 0)
                {
                    RenderAll();
                }
            }
            else
            {
                // Вывод линии
                Point point1 = pointerDictionary[id];
                Point point2 = pointerPoint.Position;

                Line line = new Line
                {
                    X1 = point1.X,
                    Y1 = point1.Y,
                    X2 = point2.X,
                    Y2 = point2.Y,
                    Stroke = new SolidColorBrush(inkDrawingAttributes.Color),
                    StrokeThickness = inkDrawingAttributes.Size.Width * 
                          pointerPoint.Properties.Pressure,
                    StrokeStartLineCap = PenLineCap.Round,
                    StrokeEndLineCap = PenLineCap.Round
                };
                newLineGrid.Children.Add(line);
                pointerDictionary[id] = point2;
            }
        }
    }

    base.OnPointerMoved(e);
}

Обратите внимание: элементы Line помещаются в панель newLineGrid, но в выводе кривых Безье используется панель contentGrid.

К моменту вызова OnPointerReleased стирание должно уже завершиться. Однако любая операция рисования должна завершиться выводом нового штриха на панели contentGrid и удаления предварительных элементов Line с панели newLineGrid:

protected override void OnPointerReleased(PointerRoutedEventArgs e)
{
    // Получение информации
    PointerPoint pointerPoint = e.GetCurrentPoint(this);
    uint id = pointerPoint.PointerId;

    if (pointerDictionary.ContainsKey(id))
    {
        // Передача PointerPoint объекту InkManager
        inkManager.ProcessPointerUp(pointerPoint);

        if (inkManager.Mode == InkManipulationMode.Inking)
        {
            // Удаление мелких сегментов
            newLineGrid.Children.Clear();

            // Вывод нового штриха
            IReadOnlyList<InkStroke> inkStrokes = inkManager.GetStrokes();
            InkStroke inkStroke = inkStrokes[inkStrokes.Count - 1];
            RenderStroke(inkStroke);
        }
        pointerDictionary.Remove(id);
    }

    base.OnPointerReleased(e);
}

Так как программа захватила указатель, она также должна содержать обработчик события PointerCaptureLost. В процессе обработки события предварительные линии удаляются с панели newLineGrid, а все остальное перерисовывается:

protected override void OnPointerCaptureLost(PointerRoutedEventArgs e)
{
    uint id = e.Pointer.PointerId;

    if (pointerDictionary.ContainsKey(id))
    {
        pointerDictionary.Remove(id);
        newLineGrid.Children.Clear();
        RenderAll();
    }
    base.OnPointerCaptureLost(e);
}
Пройди тесты
Лучший чат для C# программистов