Заметки в WinRT

89

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

Программу YellowPad нельзя назвать приложением коммерческого уровня, но она поддерживает ряд функций, отсутствующих в предыдущих программах:

Чтобы хотя бы частично отделить эту логику от пользовательского интерфейса, я определил класс с именем InkFileManager. Если бы класс InkManager не был запечатан (sealed), то InkFileManager был бы производным от InkManager; вместо этого InkFileManager создает экземпляр InkManager и объект InkDrawingAttributes и предоставляет доступ к ним в открытых свойствах. Также имеется метод для обновления InkManager новыми значениями атрибутов:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI;
using Windows.UI.Input.Inking;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

namespace WinRTTestApp
{
    public class InkFileManager
    {
        string id;
        
        // ...

        public InkFileManager(string id)
        {
            this.id = id;
            this.InkManager = new InkManager();
            this.InkDrawingAttributes = new InkDrawingAttributes();
        }

        public InkManager InkManager
        {
            private set;
            get;
        }

        public InkDrawingAttributes InkDrawingAttributes
        {
            private set;
            get;
        }

        // ...

        public void UpdateAttributes()
        {
            this.InkManager.SetDefaultDrawingAttributes(this.InkDrawingAttributes);
        }

        // ...

    }
}
public class InkFileManager
{
    // ...

    public bool IsAnythingSelected
    {
        get
        {
            bool isAnythingSelected = false;

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

            return isAnythingSelected;
        }
    }

    public void UnselectAll()
    {
        if (IsAnythingSelected)
        {
            foreach (InkStroke inkStroke in this.InkManager.GetStrokes())
                inkStroke.Selected = false;

            RenderAll();
        }
    }

    // ...
}

Также в этот файл была перемещена вся логика вывода кривых Безье. Кроме самого объекта InkManager, логике вывода необходим только объект Panel для добавления элементов Path. Эта информация предоставляется в открытом свойстве с именем RenderTarget. Вывод выделенных штрихов осуществляется так же, как в предыдущей программе:

public class InkFileManager
{
    // ...

    public Panel RenderTarget
    {
        set;
        get;
    }
    
    // ...
    
    public void RenderAll()
    {
        this.RenderTarget.Children.Clear();

        foreach (InkStroke inkStroke in this.InkManager.GetStrokes())
            RenderStroke(inkStroke);
    }

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

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

        RenderBeziers(this.RenderTarget, 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);
        }
    }

    // ...
}

Наконец, класс InkFileManager содержит два открытых метода для работы с файлами. Метод LoadAsync загружает ранее сохраненный рисунок и настройки или задает значения по умолчанию при создании новой страницы. Метод SaveAsync сохраняет текущее содержимое InkManager в локальном хранилище приложения, а также текущую толщину пера и цвет, связанные с этим объектом InkManager.

В обоих методах используется строка-идентификатор, которая передается конструктору и сохраняется в поле. Эта строка уникальна для каждого объекта InkFileManager, поддерживаемого программой. Как видно из кода, она представляет собой обычный порядковый номер (0,1,2 и т. д.), преобразованный в строку:

public class InkFileManager
{
    // ...

    bool isLoaded;
    
    // ...
    
    public async Task LoadAsync()
    {
        if (isLoaded)
            return;

        // Загрузка ранее сохраненного рисунка
        StorageFolder storageFolder = ApplicationData.Current.LocalFolder;

        try
        {
            StorageFile storageFile =
                await storageFolder.GetFileAsync("Page" + id + ".ink");

            using (IRandomAccessStream stream =
                    await storageFile.OpenAsync(FileAccessMode.Read))
            {
                await this.InkManager.LoadAsync(stream.GetInputStreamAt(0));
            }
        }
        catch
        {
            // Если происходит исключение, не делать ничего
        }

        // Загрузка сохраненных параметров
        IPropertySet appData = ApplicationData.Current.LocalSettings.Values;

        // Размер пера
        double penSize = 4;

        if (appData.ContainsKey("PenSize" + id))
            penSize = (double)appData["PenSize" + id];

        this.InkDrawingAttributes.Size = new Size(penSize, penSize);

        // Цвет
        if (appData.ContainsKey("Color" + id))
        {
            byte[] argb = (byte[])appData["Color" + id];
            this.InkDrawingAttributes.Color =
                Color.FromArgb(argb[0], argb[1], argb[2], argb[3]);
        }

        // Атрибуты вывода по умолчанию
        UpdateAttributes();
        isLoaded = true;
    }

    public async Task SaveAsync()
    {
        if (!isLoaded)
            return;

        // Сохранение рисунка
        StorageFolder storageFolder = ApplicationData.Current.LocalFolder;

        try
        {
            StorageFile storageFile =
                await storageFolder.CreateFileAsync("Page" + id + ".ink",
                                CreationCollisionOption.ReplaceExisting);

            using (IRandomAccessStream stream =
                await storageFile.OpenAsync(FileAccessMode.ReadWrite))
            {
                await this.InkManager.SaveAsync(stream.GetOutputStreamAt(0));
            }
        }
        catch
        {
            // Если происходит исключение, не делать ничего
        }

        // Сохранить настройки
        IPropertySet appData = ApplicationData.Current.LocalSettings.Values;

        // Сохранение размера пера
        appData["PenSize" + id] = this.InkDrawingAttributes.Size.Width;

        // Сохранение цвета
        Color color = this.InkDrawingAttributes.Color;
        byte[] argb = { color.A, color.R, color.G, color.B };
        appData["Color" + id] = argb;
    }

    // ...
}

В программе YellowPad каждый объект InkFileManager связывается с элементом управления YellowPadPage, производным от UserControl. Ниже приведен файл XAML этого класса, имитирующий привычный вид листка для заметок: желтый фон, две красные вертикальные линии в левой части:

<UserControl ...>

    <Grid>
        <Viewbox>
            <Grid x:Name="sheetPanel"
                  Background="#FFFF80"
                  Width="816" Height="1056">

                <Line Stroke="Red" X1="138" Y1="0" X2="138" Y2="1056" />
                <Line Stroke="Red" X1="132" Y1="0" X2="132" Y2="1056" />

                <Grid Name="contentGrid" />
                <Grid Name="newLineGrid" />
            </Grid>
        </Viewbox>
    </Grid>
</UserControl>

Элемент управления содержит встроенный элемент Viewbox, благодаря чему он адаптируется к любому размеру окна.

Как можно предположить по именам двух внутренних элементов Grid, файл фонового кода обрабатывает весь ввод с указателя. Однако я обнаружил, что при попытке заставить эту программу работать на устройстве без пера возникают проблемы. Помните, что экземпляры YellowPadPage размещаются в элементе FlipView, a FlipView нужен собственный сенсорный ввод для переключения отображаемых данных. Я решил исключить логику, которая позволяла программе работать без пера. YellowPad не будет работать без физического пера.

Конструктор YellowPadPage отвечает за рисование синей горизонтальной разметки на странице:

using System.Collections.Generic;
using System.Linq;
using Windows.Devices.Input;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Input;
using Windows.UI.Input.Inking;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

namespace WinRTTestApp
{
    public sealed partial class YellowPadPage : UserControl
    {
        public YellowPadPage()
        {
            this.InitializeComponent();

            // Рисование горизонтальных синих линий
            Brush blueBrush = new SolidColorBrush(Colors.Blue);

            for (int y = 120; y < sheetPanel.Height; y += 24)
                sheetPanel.Children.Add(new Line
                {
                    X1 = 0,
                    Y1 = y,
                    X2 = sheetPanel.Width,
                    Y2 = y,
                    Stroke = blueBrush
                });
        }
        
        // ...
    }
}

Элемент управления YellowPadPage также определяет новое свойство зависимости типа InkFileManager:

public sealed partial class YellowPadPage : UserControl
{    
    // ...
    
    static readonly DependencyProperty inkFileManagerProperty =
        DependencyProperty.Register("InkFileManager",
                        typeof(InkFileManager),
                        typeof(YellowPadPage),
                        new PropertyMetadata(null, OnInkFileManagerChanged));


    // Служебный код свойства зависимости InkFileManager
    public static DependencyProperty InkFileManagerProperty
    {
        get { return inkFileManagerProperty; }
    }

    public InkFileManager InkFileManager
    {
        set { SetValue(InkFileManagerProperty, value); }
        get { return (InkFileManager)GetValue(InkFileManagerProperty); }
    }

    static void OnInkFileManagerChanged(DependencyObject obj,
                                DependencyPropertyChangedEventArgs e)
    {
        (obj as YellowPadPage).OnInkFileManagerChanged(e);
    }

    private async void OnInkFileManagerChanged(
        DependencyPropertyChangedEventArgs e)
    {
        contentGrid.Children.Clear();
        newLineGrid.Children.Clear();

        if (e.NewValue != null)
        {
            await this.InkFileManager.LoadAsync();
            this.InkFileManager.RenderTarget = contentGrid;
            this.InkFileManager.RenderAll();
        }
    }
    
    // ...
}

Когда свойству InkFileManager задается новый экземпляр InkFileManager, обработчик изменения свойства вызывает LoadAsync для загрузки существующего рисунка и параметров, задает RenderTarget свою панель contentGrid, а затем приказывает InkFileManager вывести ранее существовавший рисунок.

Оставшаяся часть кода YellowPadPage предполагает, что свойство InkFileManager уже задано; она в основном посвящена обработке событий Pointer. Логика почти не отличается от предыдущей программы, не считая использования свойства InkFileManager для получения объектов InkManager и InkDrawingAttributes, связанных со страницей, и для вывода рисунка:

public sealed partial class YellowPadPage : UserControl
{    
    // ...
    
    Dictionary<uint, Point> pointerDictionary = new Dictionary<uint, Point>();
    Brush selectionBrush = new SolidColorBrush(Colors.Red);
    
    // ...
    
    protected override void OnPointerPressed(PointerRoutedEventArgs e)
    {
        if (e.Pointer.PointerDeviceType == PointerDeviceType.Pen)
        {
            // Получение информации
            PointerPoint pointerPoint = e.GetCurrentPoint(sheetPanel);
            uint id = pointerPoint.PointerId;
            InkManager inkManager = this.InkFileManager.InkManager;

            // Инициализация для рисования, стирания и выделения
            if (pointerPoint.Properties.IsEraser)
            {
                inkManager.Mode = InkManipulationMode.Erasing;
                this.InkFileManager.UnselectAll();
            }
            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;
                this.InkFileManager.UnselectAll();
            }

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

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

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

        base.OnPointerPressed(e);
    }

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

        if (pointerDictionary.ContainsKey(id))
        {
            foreach (PointerPoint point in e.GetIntermediatePoints(sheetPanel).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)
                    {
                        this.InkFileManager.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);
    }

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

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

                // Общий вывод с идентификацией выделенных объектов
                this.InkFileManager.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();
            this.InkFileManager.RenderAll();
        }
    }
    
    // ...
}

YellowPadPage получает экземпляр InkFileManager через привязку данных. Элемент управления FlipView в MainPage содержит коллекцию объектов InkFileManager (по одному для каждой страницы), а шаблон ItemTemplate для FlipView доминирует (во внешнем виде, не в разметке) в YellowPadPage с привязкой к объекту из коллекции ItemSource элемента управления:

<Page ... xmlns:local="using:WinRTTestApp">

    <Page.Resources>
        <local:IndexToPageNumberConverter x:Key="indexToPageNumber" />
    </Page.Resources>

    <Grid Background="#FF1D1D1D">
        <FlipView Name="flipView" SelectionChanged="OnFlipViewSelectionChanged">
            <FlipView.ItemTemplate>
                <DataTemplate>
                    <Grid HorizontalAlignment="Center" VerticalAlignment="Center">

                        <local:YellowPadPage InkFileManager="{Binding}" />

                        <TextBlock Name="pageNumTextBlock"
                                   FontSize="12"
                                   Foreground="Black"
                                   HorizontalAlignment="Right"
                                   VerticalAlignment="Top"
                                   Margin="6"
                                   Text="{Binding ElementName=flipView, 
                                                  Path=SelectedIndex,
                                   Converter={StaticResource indexToPageNumber}}" />
                    </Grid>
                </DataTemplate>
            </FlipView.ItemTemplate>
        </FlipView>
    </Grid>

    <Page.BottomAppBar>
        ...
    </Page.BottomAppBar>
</Page>
Проект: YellowPad | Файл: MainPage.xaml (фрагмент)

Элемент TextBlock, определяемый в DataTemplate вместе с YellowPadPage, выводит номер текущей страницы. В привязку свойства Text включен специальный преобразователь, который преобразует индекс (с нумерацией от нуля) в текстовую метку:

using System;
using Windows.UI.Xaml.Data;

namespace WinRTTestApp
{
    public class IndexToPageNumberConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, 
            object parameter, string language)
        {
            return String.Format("Страница {0}", (int)value + 1);
        }

        public object ConvertBack(object value, Type targetType, 
            object parameter, string language)
        {
            return value;
        }
    }
}

Как вы уже видели, каждый экземпляр InkFileManager сохраняет и восстанавливает настройки приложения, связанные со страницей, включая ее графическое содержимое. Код MainPage сохраняет и восстанавливает настройки, связанные с самим приложением. Они состоят всего из двух целочисленных значений: количества страниц (количество объектов в коллекции InkFileManager) и индекса текущей страницы (свойство Selectedlndex элемента управления FlipView):

using Windows.UI.Xaml.Controls;
using Windows.UI.Input.Inking;
using Windows.UI;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.ApplicationModel;
using Windows.Foundation.Collections;
using Windows.Storage;
using System.Collections.ObjectModel;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        ObservableCollection<InkFileManager> inkFileManagers =
                                                new ObservableCollection<InkFileManager>();
        public MainPage()
        {
            this.InitializeComponent();
            Loaded += OnMainPageLoaded;
            Application.Current.Suspending += OnApplicationSuspending;
        }

        private void OnMainPageLoaded(object sender, RoutedEventArgs e)
        {
            // Загрузка конфигурации приложения
            IPropertySet appData = ApplicationData.Current.LocalSettings.Values;

            // Получение кол-ва страниц
            int pageCount = 1;

            if (appData.ContainsKey("PageCount"))
                pageCount = (int)appData["PageCount"];

            // Создание соответствующего количества объектов InkFileManager
            for (int i = 0; i < pageCount; i++)
                inkFileManagers.Add(new InkFileManager(i.ToString()));

            // Включении коллекции в FlipView
            flipView.ItemsSource = inkFileManagers;

            // Задание свойства SelectedIndex объекта PageView
            if (appData.ContainsKey("PageIndex"))
                flipView.SelectedIndex = (int)appData["PageIndex"];
        }

        private async void OnApplicationSuspending(object sender, SuspendingEventArgs e)
        {
            SuspendingDeferral deferral = e.SuspendingOperation.GetDeferral();

            // Сохранение всего содержимого InkFileManager
            foreach (InkFileManager inkFileManager in inkFileManagers)
                await inkFileManager.SaveAsync();

            // Сохранение количества страниц и индекса текущей страницы
            IPropertySet appData = ApplicationData.Current.LocalSettings.Values;
            appData["PageCount"] = inkFileManagers.Count;
            appData["PageIndex"] = flipView.SelectedIndex;

            deferral.Complete();
        }

        private void OnFlipViewSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // Если это последний объект FlipView, создать новый!
            if (flipView.SelectedIndex == flipView.Items.Count - 1)
                inkFileManagers.Add(new InkFileManager(flipView.Items.Count.ToString()));
        }
        
        // ...
        
    }
}

Обработчик Loaded создает все объекты InkFileManager для текущего количества страниц, но конструктор InkFileManager не делает ничего, кроме создания экземпляров InkManager и InkDrawingAttributes. В частности, он не загружает ранее сохраненный рисунок. Это происходит позднее, когда экземпляр InkFileManager непосредственно связывается с YellowPadPage.

Не забывайте, что FlipView использует панель VirtualizingStackPanel, которая создает визуальные деревья для вариантов только по мере надобности. Это означает, что загрузка ранее сохраненного рисунка откладывается на какое-то время и происходит только при активном переборе страниц пользователем. Некоторые страницы могут быть вообще не загружены и их не нужно сохранять заново.

Оставшаяся часть кода относится к обработке кнопок в строке приложения, включая описанную ранее несовершенную логику вставки. Кроме четырех кнопок, относящихся к операциям с буфером обмена, в строке приложения также расположены два очень похожих шаблонных элемента управления ComboBox: для толщины и цвета пера:

<Page ...>

    ...

    <Page.BottomAppBar>
        <AppBar Name="bottomAppBar" Opened="OnAppBarOpened">
            <Grid>
                <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>

                <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
                    <ComboBox Name="penSizeComboBox"
                              SelectionChanged="OnPenSizeComboBoxSelectionChanged"
                              Width="200" Margin="20 0">
                        <x:Double>2</x:Double>
                        <x:Double>3</x:Double>
                        <x:Double>4</x:Double>
                        <x:Double>5</x:Double>
                        <x:Double>7</x:Double>
                        <x:Double>10</x:Double>

                        <ComboBox.ItemTemplate>
                            <DataTemplate>
                                <Path StrokeThickness="{Binding}"
                                      Stroke="Black"
                                      StrokeStartLineCap="Round"
                                      StrokeEndLineCap="Round"
                                      Data="M 0 0 C 50 20 100 0 150 20" />
                            </DataTemplate>
                        </ComboBox.ItemTemplate>
                    </ComboBox>

                    <ComboBox Name="colorComboBox"
                              SelectionChanged="OnColorComboBoxSelectionChanged"
                              Width="200"
                              Margin="20 0">
                        <Color>#FF0000</Color>
                        <Color>#800000</Color>
                        <Color>#FFFF00</Color>
                        <Color>#808000</Color>
                        <Color>#00FF00</Color>
                        <Color>#008000</Color>
                        <Color>#00FFFF</Color>
                        <Color>#008080</Color>
                        <Color>#0000FF</Color>
                        <Color>#000080</Color>
                        <Color>#FF00FF</Color>
                        <Color>#800080</Color>
                        <Color>#C0C0C0</Color>
                        <Color>#808080</Color>
                        <Color>#404040</Color>
                        <Color>#000000</Color>

                        <ComboBox.ItemTemplate>
                            <DataTemplate>
                                <Path StrokeThickness="6"
                                      StrokeStartLineCap="Round"
                                      StrokeEndLineCap="Round"
                                      Data="M 0 0 C 50 20 100 0 150 20">
                                    <Path.Stroke>
                                        <SolidColorBrush Color="{Binding}" />
                                    </Path.Stroke>
                                </Path>
                            </DataTemplate>
                        </ComboBox.ItemTemplate>
                    </ComboBox>
                </StackPanel>
            </Grid>
        </AppBar>
    </Page.BottomAppBar>
</Page>

Чтобы упростить программу, я не стал реализовывать коррекцию для книжной ориентации и режимов Snap View. В этих режимах кнопки и поля перекрываются.

Все элементы управления в строке приложения относятся к текущей странице, отображаемой в FlipView. Более того, два элемента управления ComboBox могут относиться как к странице (то есть к объекту InkDrawingAttributes по умолчанию, связанному с текущим объектом InkFileManager этой страницы), так и к выделенным объектам на странице. При открытии строки приложения эти элементы управления необходимо инициализировать соответствующим образом:

private void OnAppBarOpened(object sender, object e)
{
    InkFileManager inkFileManager = (InkFileManager)flipView.SelectedItem;

    copyAppBarButton.IsEnabled = inkFileManager.IsAnythingSelected;
    cutAppBarButton.IsEnabled = inkFileManager.IsAnythingSelected;
    pasteAppBarButton.IsEnabled = inkFileManager.InkManager.CanPasteFromClipboard();
    deleteAppBarButton.IsEnabled = inkFileManager.IsAnythingSelected;

    if (!inkFileManager.IsAnythingSelected)
    {
        // Назначение изначально выбранного варианта
        Size size = inkFileManager.InkDrawingAttributes.Size;
        penSizeComboBox.SelectedItem = (size.Width + size.Height) / 2;
        colorComboBox.SelectedItem = inkFileManager.InkDrawingAttributes.Color;
    }
    else
    {
        penSizeComboBox.SelectedItem = null;
        colorComboBox.SelectedItem = null;
    }
}

Более совершенная версия этого метода могла бы перебирать выделенные штрихи и проверять, имеют ли они одинаковый цвет или толщину. В случае совпадения эти значения могли бы использоваться для инициализации двух элементов управления ComboBox. В текущей версии при наличии выделенных штрихов в элементах управления ComboBox значение не выбирается.

Код четырех кнопок для операций с буфером очень похож на код из предыдущей программы, не считая того, что обращение к InkManager должно осуществляться через объект InkFileManager, содержащийся в свойстве SelectedItem объекта FlipView:

// ...

private void OnCopyAppBarButtonClick(object sender, RoutedEventArgs e)
{
    InkFileManager inkFileManager = (InkFileManager)flipView.SelectedItem;
    inkFileManager.InkManager.CopySelectedToClipboard();

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

    inkFileManager.RenderAll();
    bottomAppBar.IsOpen = false;
}

private void OnCutAppBarButtonClick(object sender, RoutedEventArgs e)
{
    InkFileManager inkFileManager = (InkFileManager)flipView.SelectedItem;
    inkFileManager.InkManager.CopySelectedToClipboard();
    inkFileManager.InkManager.DeleteSelected();
    inkFileManager.RenderAll();
    bottomAppBar.IsOpen = false;
}

private void OnPasteAppBarButtonClick(object sender, RoutedEventArgs e)
{
    InkFileManager inkFileManager = (InkFileManager)flipView.SelectedItem;
    inkFileManager.InkManager.PasteFromClipboard(new Point());
    inkFileManager.RenderAll();
    bottomAppBar.IsOpen = false;
}

private void OnDeleteAppBarButtonClick(object sender, RoutedEventArgs e)
{
    InkFileManager inkFileManager = (InkFileManager)flipView.SelectedItem;
    inkFileManager.InkManager.DeleteSelected();
    inkFileManager.RenderAll();
    bottomAppBar.IsOpen = false;
}
        
// ...

Логика двух элементов управления ComboBox очень похожа. В обоих случаях либо объект InkDrawingAttributes, связанный с InkFileManager, получает новые значения для будущего рисования, либо выделенные штрихи обновляются новыми значениями:

private void OnPenSizeComboBoxSelectionChanged(object sender, 
    SelectionChangedEventArgs e)
{
    if (penSizeComboBox.SelectedItem == null)
        return;

    InkFileManager inkFileManager = (InkFileManager)flipView.SelectedItem;

    double penSize = (double)penSizeComboBox.SelectedItem;
    Size size = new Size(penSize, penSize);

    if (!inkFileManager.IsAnythingSelected)
    {
        inkFileManager.InkDrawingAttributes.Size = size;
        inkFileManager.UpdateAttributes();
    }
    else
    {
        foreach (InkStroke inkStroke in inkFileManager.InkManager.GetStrokes())
            if (inkStroke.Selected)
            {
                InkDrawingAttributes drawingAttrs = inkStroke.DrawingAttributes;
                drawingAttrs.Size = size;
                inkStroke.DrawingAttributes = drawingAttrs;
            }
        inkFileManager.RenderAll();
    }
}

private void OnColorComboBoxSelectionChanged(object sender, 
   SelectionChangedEventArgs e)
{
    if (colorComboBox.SelectedItem == null)
        return;

    InkFileManager inkFileManager = (InkFileManager)flipView.SelectedItem;

    Color color = (Color)colorComboBox.SelectedItem;

    if (!inkFileManager.IsAnythingSelected)
    {
        inkFileManager.InkDrawingAttributes.Color = color;
        inkFileManager.UpdateAttributes();
    }
    else
    {
        foreach (InkStroke inkStroke in inkFileManager.InkManager.GetStrokes())
            if (inkStroke.Selected)
            {
                InkDrawingAttributes drawingAttrs = inkStroke.DrawingAttributes;
                drawingAttrs.Color = color;
                inkStroke.DrawingAttributes = drawingAttrs;
            }
            
        inkFileManager.RenderAll();
    }
}

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

Приложение Windows Runtime для создания заметок

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

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