Заметки в WinRT
89Разработка под Windows 10 --- Заметки
В работе над этим руководством я часто использовал желтые листки с клеевым краем. Это мой любимый инструмент для заметок, записи идей, планирования связей между блоками кода и решения математических задач. Не знаю, перейду ли я когда-нибудь на электронные листки для заметок, но я хотя бы дам им шанс и запрограммирую приложение, которое позволит мне опробовать их в деле.
Программу YellowPad нельзя назвать приложением коммерческого уровня, но она поддерживает ряд функций, отсутствующих в предыдущих программах:
Приложение YellowPad поддерживает многостраничные заметки, просматриваемые в элементе управления FlipView. Чтобы перейти от страницы к странице, достаточно провести пальцем по экрану.
Программа следит за тем, чтобы элемент управления FlipView не достиг конца коллекции; когда это должно произойти, всегда создается новая страница.
В приложении YellowPad также задействованы методы LoadAsync и SaveAsync, определяемые объектом InkManager; содержимое всех страниц сохраняется в локальном хранилище приложения во время события Suspending и загружается при следующем запуске программы.
В приложении используются уже описанные ранее кнопки строки приложения. К ним добавляются новые средства для выбора толщины пера и цвета штрихов текущей страницы (или выделенных штрихов).
Чтобы хотя бы частично отделить эту логику от пользовательского интерфейса, я определил класс с именем 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>
Элемент 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.
Еще в этой программе был бы уместен элемент управления GridView, который выводит миниатюры всех страниц и позволяет перемещаться между ними, выбирать страницы для удаления или печати и даже группировать их.