Редактирование и контекстное меню в WinRT

191

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

В двух предыдущих программах объект Polyline создавался, инициализировался и добавлялся на панель Grid и в словарь следующим образом:

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

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

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

В программе FingerPaint3 появляется дополнительный код, который назначает для Polyline обработчик событий. В обработчике события RightTapped объекта Polyline будет отображаться контекстное меню:

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.PointerPressed += polyline_PointerPressed;
    polyline.RightTapped += polyline_RightTapped;
    polyline.Points.Add(point);
    polyline.Points.Add(point);

     // ...

}

И хотя нас интересует только событие RightTapped объекта Polyline, я также назначил обработчик события PointerPressed. Он выглядит не очень интересно, но играет очень важную роль:

private void polyline_PointerPressed(object sender, PointerRoutedEventArgs e)
{
    e.Handled = true;
}

Обязательно попробуйте, как работает эта программа без обработчика. Когда срабатывает событие PointerPressed, оно ассоциируется с верхним элементом, доступным для пользовательского ввода. Если щелкнуть на объекте Polyline вместо поверхности MainPage, то для этого объекта инициируется событие PointerPressed.

Однако событие PointerPressed является маршрутизируемым, а как говорилось ранее, такие события передаются вверх по визуальному дереву. Это означает, что если событие не представляет интереса для объекта Polyline, оно будет передано странице MainPage, которая будет считать, что вы хотите нарисовать новую фигуру. Чтобы этого не произошло, Polyline обрабатывает событие PointerPressed, устанавливая свойству Handled в аргументах события значение true. Тем самым предотвращается передача события MainPage.

Логика контекстного меню реализуется в событии RightTapped:

private async void polyline_RightTapped(object sender, RightTappedRoutedEventArgs e)
{
    Polyline polyline = sender as Polyline;
    PopupMenu popupMenu = new PopupMenu();
    popupMenu.Commands.Add(new UICommand("Изменить цвет", menu_ChangeColor, polyline));
    popupMenu.Commands.Add(new UICommand("Удалить", menu_Delete, polyline));
    await popupMenu.ShowAsync(e.GetPosition(this));
}

Использовать класс PopupMenu достаточно просто. После создания объекта в меню можно добавить до шести команд. Каждая команда состоит из текстовой метки, метода обратного вызова и дополнительного объекта, который помогает методу обратного вызова идентифицировать событие. Метод ShowAsync() выводит меню в заданной позиции.

Обработчики могут получить последний аргумент, переданный конструктору UICommand, преобразуя тип свойства Id аргумента IUICommand метода обратного вызова:

private void menu_ChangeColor(IUICommand command)
{
    Polyline polyline = command.Id as Polyline;
    random.NextBytes(rgb);
    Color color = Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);
    (polyline.Stroke as SolidColorBrush).Color = color;
}

private void menu_Delete(IUICommand command)
{
    Polyline polyline = command.Id as Polyline;
    grid.Children.Remove(polyline);
}

Конечно вы уже знаете как использовать мышь для щелчка правой кнопкой на объекте Polyline. С сенсорным вводом необходимо ненадолго задержать палец на Polyline, а потом отпустить. При достижении нужной продолжительности нажатия на экране появляется квадрат. Аналогичным образом дело обстоит и с пером: удерживайте нажатие до тех пор, пока не появится круг, а затем отпустите. На экране появляется контекстное меню:

Вызов контекстного меню в программе рисования

Квадрат и круг, которые вы видите при удерживании пальца или пера у экрана, в действительности связаны с событием Holding. Если задать свойству IsHoldingEnabled объекта Polyline значение false, они не появятся, и пользователь может засомневаться, как долго еще нужно удерживать нажатие. Событие RightTapped не инициируется до тех пор, пока пользователь не отнимет палец или перо от экрана.

Метод OnMenuDelete в FingerPaint3 в действительности содержит неочевидную ошибку. Если один палец пользователя рисует линию, а другой - вызывает меню для этой линии, OnMenuDelete удалит Polyline с экрана , но не соответствующий объект словаря. Ничего плохого не произойдет, но в словаре будут постепенно накапливаться бесхозные объекты. Проблема решается поиском по словарю удаленного объекта Polyline и удалением ключа этого объекта.

Как было показано ранее, для маршрутизируемых событий, при работе с событиями, сгенерированными разными элементами, обработку событий можно структурировать по-разному. Например, переопределение OnPointerPressed в MainPage может включать логику, которую я разместил в OnPolylinePointerPressed, а вся обработка RightTapped может выполняться в переопределении OnRightTapped. От вас потребуется лишь проверить свойство OriginalSource в аргументах события, чтобы определить откуда поступил ввод — от Polyline или MainPage.

Теперь у программы появился небольшой недостаток. Вы не сможете нарисовать новую линию, если она должна начинаться с точки, занятой существующей линией. Любое событие PointerPressed, получаемое Polyline, помечается как обработанное (Handled) и уничтожается.

А если вы хотите предоставить пользователю обе возможности? Если пользователь нажимает на существующем объекте Polyline и перемещает палец, создается новая фигура. Если пользователь нажимает и удерживает палец на экране, открывается меню.

Вероятно, проще всего будет отказаться от использования события RightTapped и обрабатывать все через логику Pointer. Когда событие OnPointerPressed происходит в существующем объекте Polyline, программа устанавливает таймер DispatcherTimer на одну секунду, но отменяет этот таймер (и начинает операцию рисования) при возникновении события OnPointerMoved, обозначающего, что палец сместился на расстояние, превышающее некоторый заранее заданный критерий. При срабатывании таймера на экране появляется меню.

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