Выделение штрихов в WinRT

147

Третье значение из перечисления InkManipulationMode — Selecting — обозначает режим выделения. При использовании электромагнитного пера объект InkManager переводится в режим выделения по событию PointerPressed, если нажата кнопка пера. В нашем примере используется именно этот способ, но в реальном приложении также следует предусмотреть программные средства для ручного перевода InkManager в режим выделения.

В этом режиме точки, передаваемые методу ProcessPointerUpdate, интерпретируются как определение замкнутой области. Вероятно, линия выделения должна отображаться, но так, чтобы она отличалась от обычного перьевого ввода. При завершении линии выделения ProcessPointerUp возвращает непустое значение Rect, обозначающее ограничивающий прямоугольник выделенных штрихов. Если ни один штрих не выделен, объект Rect пуст. При наличии выделенных штрихов у их объектов InkStroke в коллекции свойству Selected задается значение true.

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

Также возможно выделение штрихов программными средствами, с использованием методов SelectWithLine и SelectWithPolyLine объекта InkManager и ручного переключения свойства Selected объекта InkStroke, но я не буду демонстрировать эти приемы. Они позволяют реализовать ваш собственный протокол выделения, не зависящий от InkManager; в этом случае вы просто не вызываете методы InkManager во время операции выделения.

После того как пользователь выделит один или несколько объектов InkStroke, их необходимо каким-то образом визуально пометить. Также необходимо реализовать программные средства для выполнения каких-то операций с выделенными объектами. Сам класс InkManager определяет методы с именами DeleteSelected, CopySelectedToClipboard и MoveSelected. Последний метод перемещает штрихи с конкретным смещением относительно их текущей позиции. Также возможна вставка штрихов из буфера обмена в InkManager.

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

В следующем проекте с именем InkEraseSelect продемонстрированы все три режима. Как и в InkAndErase, в нем имеются два элемента Grid для вывода предварительных линий и итогового рисунка:

<Page ...>

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

    <Page.BottomAppBar>
        <AppBar Name="bottomAppBar"
                Opened="OnAppBarOpened">
            <StackPanel Orientation="Horizontal"
                        HorizontalAlignment="Left">

                <AppBarButton Name="copyAppBarButton"
                              Icon="Copy" Label="Копировать"
                              Click="OnCopyAppBarButtonClick" />

                <AppBarButton Name="cutAppBarButton"
                              Icon="Cut" Label="Вставить"
                              Click="OnCutAppBarButtonClick" />

                <AppBarButton Name="pasteAppBarButton"
                              Icon="Paste" Label="Вставить"
                              Click="OnPasteAppBarButtonClick" />

                <AppBarButton Name="deleteAppBarButton"
                              Icon="Delete" Label="Удалить"
                              Click="OnDeleteAppBarButtonClick" />
            </StackPanel>
        </AppBar>
    </Page.BottomAppBar>
</Page>

Файл XAML также содержит набор кнопок строки приложения для стандартных операций копирования, вырезания, вставки и удаления.

Файл фонового кода начинается практически так же, как в предыдущей программе, не считая определения объекта Brush для вывода ограничивающей линии выделенных объектов:

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;
using Windows.UI;
using Windows.Foundation;
using Windows.UI.Xaml;

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

        Dictionary<uint, Point> pointerDictionary = new Dictionary<uint, Point>();
        Brush selectionBrush = new SolidColorBrush(Colors.LimeGreen);

        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);
        }

        // ...
    }
}

Переопределение OnPointerPressed также проверяет кнопку пера. Если кнопка нажата, выбирается режим выделения. (В реальной программе следует предусмотреть возможность включения этого режима при отсутствии кнопки пера.) В режиме выделения программа рисует простой контур однородной толщины, поэтому она создает для этой цели объект Polyline и добавляет его к newLineGrid:

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.Erasing;
        }
        else if (pointerPoint.Properties.IsBarrelButtonPressed)
        {
            inkManager.Mode = InkManipulationMode.Selecting;

            // Создание объекта Polyline для вывода ограничивающего контура
            Polyline polyline = new Polyline
            {
                Stroke = selectionBrush,
                StrokeThickness = 1
            };
            polyline.Points.Add(pointerPoint.Position);
            newLineGrid.Children.Add(polyline);
        }
        else
        {
            inkManager.Mode = InkManipulationMode.Inking;
        }

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

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

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

В переопределении OnPointerMoved режимы стирания и рисования реализованы так же, как в предыдущей программе. В режиме выделения объект Polyline просто продолжается, как в программе FingerPaint:

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())
        {
            Point point1 = pointerDictionary[id];
            Point point2 = pointerPoint.Position;

            // Передача 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 if (inkManager.Mode == InkManipulationMode.Selecting)
            {
                Polyline polyline = newLineGrid.Children[0] as Polyline;
                polyline.Points.Add(point2);
            }
            else // inkManager.Mode == InkManipulationMode.Inking
            {
                // Вывод линии
                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);
}

Конечно, в программе FingerPaint приходилось отслеживать несколько элементов Polyline для нескольких пальцев, прикасающихся к экрану, и эти элементы Polyline хранились в словаре. Хотя объект InkManager может обрабатывать действия нескольких пальцев, он не поддерживает несколько перьев, а поскольку в этой программе выделение поддерживается только для пера, существование множественных элементов Polyline невозможно. Возможно, программе с альтернативными средствами выделения, основанными на сенсорном вводе, придется иметь дело с определением сразу нескольких областей выделения!

В режиме выделения переопределение OnPointerReleased удаляет элемент Polyline, определяющий границу, и вызывает RenderAll. Логика графического вывода отвечает за отличия в визуализации выделенных штрихов:

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);
        }
        else if (inkManager.Mode == InkManipulationMode.Selecting)
        {
            // Удаление ограничивающего контура
            newLineGrid.Children.Clear();

            // Общий вывод с идентификацией выделенных объектов
            RenderAll();
        }

        pointerDictionary.Remove(id);
    }

    base.OnPointerReleased(e);
}

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);
}

Вот как выглядит приложение непосредственно перед завершением выделения и отходом пера, когда ограничивающая линия удаляется с экрана:

Выделение контуров с помощью пера

В этой программе я разделил логику вывода на три метода. Метод RenderStroke вызывает RenderBeziers, но для выделенных штрихов это делается дважды; при первом вызове используется измененный цвет и увеличенное перо, чтобы штрихи были окружены контуром выделения:

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

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

private void RenderStroke(InkStroke inkStroke)
{
    Color color = inkStroke.DrawingAttributes.Color;
    double penSize = inkStroke.DrawingAttributes.Size.Width;

    if (inkStroke.Selected)
        RenderBeziers(contentGrid, inkStroke, Colors.Silver, penSize + 24);

    RenderBeziers(contentGrid, inkStroke, color, penSize);
}

static void RenderBeziers(Panel panel, InkStroke inkStroke, Color color, double penSize)
{
    Brush brush = new SolidColorBrush(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 = penSize * inkSegment.Pressure,
            StrokeStartLineCap = PenLineCap.Round,
            StrokeEndLineCap = PenLineCap.Round,
            Data = pathGeometry
        };
        panel.Children.Add(path);
    }
}

Я сделал метод RenderBeziers статическим, чтобы продемонстрировать, какие именно параметры необходимы методу для вывода одного штриха. Вот как выглядят выделенные штрихи при таком способе идентификации:

Выделенные штрихи при перьевом вводе

Конечно, это всего лишь один из возможных способов обозначения выделенных штрихов; возможно, вы выберете что-то другое.

При открытой строке приложения обработчик Opened устанавливает или снимает блокировку четырех кнопок. Доступность трех кнопок определяется присутствием выделенных штрихов в InkManager; доступность кнопки вставки определяется свойством CanPasteFromClipboard объекта InkManager:

private void OnAppBarOpened(object sender, object e)
{
    bool isAnythingSelected = false;

    foreach (InkStroke inkStroke in inkManager.GetStrokes())
        isAnythingSelected |= inkStroke.Selected;

    copyAppBarButton.IsEnabled = isAnythingSelected;
    cutAppBarButton.IsEnabled = isAnythingSelected;
    pasteAppBarButton.IsEnabled = inkManager.CanPasteFromClipboard();
    deleteAppBarButton.IsEnabled = isAnythingSelected;
}

private void OnCopyAppBarButtonClick(object sender, RoutedEventArgs e)
{
    inkManager.CopySelectedToClipboard();

    foreach (InkStroke inkStroke in inkManager.GetStrokes())
        inkStroke.Selected = false;

    RenderAll();
    bottomAppBar.IsOpen = false;
}

private void OnCutAppBarButtonClick(object sender, RoutedEventArgs e)
{
    inkManager.CopySelectedToClipboard();
    inkManager.DeleteSelected();
    RenderAll();
    bottomAppBar.IsOpen = false;
}

private void OnPasteAppBarButtonClick(object sender, RoutedEventArgs e)
{
    inkManager.PasteFromClipboard(new Point());
    RenderAll();
    bottomAppBar.IsOpen = false;
}

private void OnDeleteAppBarButtonClick(object sender, RoutedEventArgs e)
{
    inkManager.DeleteSelected();
    RenderAll();
    bottomAppBar.IsOpen = false;
}

Логика кнопки Copy предполагает, что штрихи не должны оставаться выделенными после копирования в буфер обмена. В обработчиках Cut и Delete этого делать не нужно, потому что выделенные штрихи исчезли из рисунка.

Интересно, что при копировании рисунка в буфер объект InkManager также преобразует рисунок в растровое изображение и расширенный метафайл, так что эти форматы буфера обмена также доступны для вставки. Некоторые программы (и прежде всего Microsoft Word) могут читать рисунок прямо из буфера, но программы с возможностью вставки растровых изображений из буфера встречаются очень часто.

Все координаты в рисунке, копируемом в буфер, нормализуются по минимальным координатам (0, 0). Именно по этой причине методу PasteFromClipboard необходим аргумент Point. Если он не задан (как у меня), вставленный рисунок отображается в левом верхнем углу. Реальная программа с функцией вставки должна предоставить пользователю возможность задать местонахождение вставленного рисунка на странице. Аналогичная логика также может использоваться для реализации метода MoveSelected, поддерживаемого InkManager.

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