Рукописный ввод в 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) 
2{ 
    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}"/>

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

InkPresenter

Исходные коды: InkTest.zip

Реклама