Рукописный ввод в Windows Phone 7

Ввод информации в Windows Phone 7 осуществляется путем использования встроенной программной или аппаратной (на некоторых устройствах) клавиатуры. Программная клавиатура способна подстраиваться под текущую ситуацию, в которой находится пользователь. Тем не менее, иногда может потребоваться нарисовать что-то на экране – в этом случае необходим рукописный ввод.

Рукописный ввод хорошо использовать когда нужно что-то нарисовать – небольшую картинку, например, или оставить свою подпись на экране. Такие задачи встречаются не слишком часто, но тем не менее они появляются. Для того, чтобы реализовать в приложении такую функциональность можно воспользоваться объектом InkPresenter. Для этого просто добавим его на форму.

<phoneNavigation:PhoneApplicationPage 
    x:Class="InkTest.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phoneNavigation="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Navigation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="800"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone.Shell"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}">

    <Grid x:Name="LayoutRoot" Background="{StaticResource PhoneBackgroundBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!--TitleGrid is the name of the application and page title-->
        <Grid x:Name="TitleGrid" Grid.Row="0">
            <TextBlock Text="MY APPLICATION" x:Name="textBlockPageTitle" Style="{StaticResource PhoneTextPageTitle1Style}"/>
            <TextBlock Text="Ink" x:Name="textBlockListTitle" Style="{StaticResource PhoneTextPageTitle2Style}"/>
        </Grid>

        <!--ContentGrid is empty. Place new content here-->
        <Grid x:Name="ContentGrid" Grid.Row="1">
            <InkPresenter Background="White" x:Name="ink1" />
        </Grid>
    </Grid>
    
</phoneNavigation:PhoneApplicationPage>

Однако, простого добавления элемента на форму будет недостаточно. После добавление необходимо обработать несколько событий – MouseMove, MouseLeftButtonDown и MouseLeftButtonUp.

Дело в том, что объект InkPresenter содержит коллекцию Strokes, которая объекты Stroke. Stroke — это набор точек, нарисованных пользователем. Наша задача заключается в том, чтобы при нажатии и перемещении указателя сохранять все координаты в объект Stroke и при отпускании добавить все эти точки в коллекцию Strokes объекта InkPresenter.

Для этих целей при нажатии на экран мы создадим новый объект Stroke и при перемещении указателя будем добавлять каждую точку в этот объект. Для этих целей будем использовать события MouseLeftButtonDown и MouseMove.

public MainPage()
{
    InitializeComponent();

    SupportedOrientations = SupportedPageOrientation.Portrait | SupportedPageOrientation.Landscape;
    ink1.MouseMove += new MouseEventHandler(ink1_MouseMove);
    ink1.MouseLeftButtonDown += new MouseButtonEventHandler(ink1_MouseLeftButtonDown);
}

private Stroke _currentStroke; 

void ink1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    ink1.CaptureMouse();
    _currentStroke = new Stroke();
    _currentStroke.DrawingAttributes.Color = Colors.Red;

    var currentPosition = e.GetPosition(ink1);
    _currentStroke.StylusPoints.Add(new StylusPoint(currentPosition.X, currentPosition.Y));
    
    ink1.Strokes.Add(_currentStroke);  
}

void ink1_MouseMove(object sender, MouseEventArgs e)
{
    if (_currentStroke != null)
    {
        var currentPosition = e.GetPosition(ink1);
        _currentStroke.StylusPoints.Add(new StylusPoint(currentPosition.X, currentPosition.Y));
    }
}

После того, как пользователь нарисовал то, что хотел, нужно просто присвоить полю _currentStroke пустое значение (для того, чтобы больше не обрабатывалось событие MouseMove).

void ink1_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    _currentStroke = null;  
}

После этого рукописный ввод уже будет работать. Давайте немного усовершенствуем наше приложение и добавим возможность отмены последнего ввода. Как вы, наверное, догадались, для этого необходимо удалить последний объект Stroke из коллекции Strokes.

Добавим меню, в котором будет пункт, позволяющий отменять последнее действие. Для этого сделаем ссылку на сборку Microsoft.Phone.Shell и определим пространство имен shell в XAML.

<phoneNavigation:PhoneApplicationPage 
    x:Class="InkTest.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phoneNavigation="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Navigation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="800"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone.Shell"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}">

<!-- содержимое формы -->

</phoneNavigation:PhoneApplicationPage>

Осталось только определить содержимое меню и создать для него обработчик.

<phoneNavigation:PhoneApplicationPage 
    x:Class="InkTest.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phoneNavigation="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Navigation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="800"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone.Shell"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}">
    <phoneNavigation:PhoneApplicationPage.ApplicationBar>
        <shell:ApplicationBar IsMenuEnabled="True">
            <shell:ApplicationBar.MenuItems>
                <shell:ApplicationBarMenuItem Text="Undo" Click="ApplicationBarMenuItem_Click"/>
            </shell:ApplicationBar.MenuItems>
        </shell:ApplicationBar>
    </phoneNavigation:PhoneApplicationPage.ApplicationBar>

    <Grid x:Name="LayoutRoot" Background="{StaticResource PhoneBackgroundBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!--TitleGrid is the name of the application and page title-->
        <Grid x:Name="TitleGrid" Grid.Row="0">
            <TextBlock Text="MY APPLICATION" x:Name="textBlockPageTitle" Style="{StaticResource PhoneTextPageTitle1Style}"/>
            <TextBlock Text="Ink" x:Name="textBlockListTitle" Style="{StaticResource PhoneTextPageTitle2Style}"/>
        </Grid>

        <!--ContentGrid is empty. Place new content here-->
        <Grid x:Name="ContentGrid" Grid.Row="1">
            <InkPresenter Background="White" x:Name="ink1" />
        </Grid>
    </Grid>
    
</phoneNavigation:PhoneApplicationPage>

Обработчик для отмены ввода будет выглядеть очень просто – в нем мы проверим вводил ли пользователь что-либо, и если да, то удалим этот фрагмент (объект Stroke).

private void ApplicationBarMenuItem_Click(object sender, EventArgs e)
{
    if (ink1.Strokes != null && ink1.Strokes.Count > 0)
    {
        ink1.Strokes.RemoveAt(ink1.Strokes.Count - 1);
    }  
}  

Теперь у нас есть возможность отменить последний ввод. Но что, делать, если мы отменили его случайно? Логично, что рядом с Undo должен быть и пункт Redo. Сделать его очень просто – нужно просто вернуть объект Stroke обратно в коллекцию Strokes. Давайте добавим еще один пункт меню и создадим для него обработчик.

<phoneNavigation:PhoneApplicationPage 
    x:Class="InkTest.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phoneNavigation="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Navigation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="800"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone.Shell"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}">
    <phoneNavigation:PhoneApplicationPage.ApplicationBar>
        <shell:ApplicationBar IsMenuEnabled="True">
            <shell:ApplicationBar.MenuItems>
                <shell:ApplicationBarMenuItem Text="Undo" Click="ApplicationBarMenuItem_Click"/>
                <shell:ApplicationBarMenuItem Text="Redo" Click="ApplicationBarMenuItem_Click_1"/>
            </shell:ApplicationBar.MenuItems>
        </shell:ApplicationBar>
    </phoneNavigation:PhoneApplicationPage.ApplicationBar>

    <Grid x:Name="LayoutRoot" Background="{StaticResource PhoneBackgroundBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!--TitleGrid is the name of the application and page title-->
        <Grid x:Name="TitleGrid" Grid.Row="0">
            <TextBlock Text="MY APPLICATION" x:Name="textBlockPageTitle" Style="{StaticResource PhoneTextPageTitle1Style}"/>
            <TextBlock Text="Ink" x:Name="textBlockListTitle" Style="{StaticResource PhoneTextPageTitle2Style}"/>
        </Grid>

        <!--ContentGrid is empty. Place new content here-->
        <Grid x:Name="ContentGrid" Grid.Row="1">
            <InkPresenter Background="White" x:Name="ink1" />
        </Grid>
    </Grid>
    
</phoneNavigation:PhoneApplicationPage>

Модифицируем обработчик отмены ввода таким образом, чтобы он сохранял удаленный объект. Обработчик для восстановления объекта просто будет добавлять к коллекцию этот удаленный объект. Сам удаленный объект будем хранить в локальном поле.

private void ApplicationBarMenuItem_Click(object sender, EventArgs e)
{
    if (ink1.Strokes != null && ink1.Strokes.Count > 0)
    {
        _canceledStroke = ink1.Strokes.Last();
        ink1.Strokes.RemoveAt(ink1.Strokes.Count - 1);
    }  
}

Stroke _canceledStroke = null;

private void ApplicationBarMenuItem_Click_1(object sender, EventArgs e)
{
    if (_canceledStroke != null)
    {
        ink1.Strokes.Add(_canceledStroke);
        _canceledStroke = null;
    }
}  

Понятно, что хранить только последнюю отмену – не очень правильно. Гораздо правильнее – хранить список, или лучше стек всех отмененных вводов. Но это я оставлю вам для тренировки пальцев.

Ну и, наконец, давайте отобразим количество объектов в коллекции Strokes для большей наглядности. Я привяжу свойство Count к тексту на форме.

<TextBlock Text="{Binding ElementName=ink1, Path=Strokes.Count}" x:Name="textBlockPageTitle" Style="{StaticResource PhoneTextPageTitle1Style}"/>

Теперь можно запустить приложение и попробовать нарисовать что-то очень красивое. Например, яблоко :).