Строка приложения для текстового редактора в WinRT
131WinRT --- Строка приложения для текстового редактора
В статье «Файловый ввод/вывод в WinRT» мы создали программу, в которой в верхней части страницы располагались три кнопки: для открытия и сохранения файла, а также переключатель ToggleButton для включения/выключения переноса. Давайте преобразуем их в кнопки строки приложения, а также реализуем функцию включения/выключения переноса в виде Popup и добавим кнопки для увеличения/уменьшения размера шрифта. Логика файлового ввода/вывода при этом останется на прежнем уровне. Файл MainPage.xaml выглядит так:
<Page ...>
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<TextBox Name="txtbox"
IsEnabled="False"
FontSize="28"
AcceptsReturn="True" Margin="0 0 0 50" />
</Grid>
<Page.BottomAppBar>
<AppBar>
<Grid>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Left">
<AppBarButton Click="OnFontIncreaseAppBarButtonClick"
Icon="FontIncrease"
Label="Увеличить шрифт" />
<AppBarButton Click="OnFontDecreaseAppBarButtonClick"
Icon="FontDecrease"
Label="Уменьшить шрифт" />
<AppBarButton Click="OnWrapOptionAppBarButtonClick"
Icon="Setting"
Label="Перенос текста" />
</StackPanel>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right">
<AppBarButton Click="OnOpenAppBarButtonClick"
Icon="OpenFile"
Label="Открыть файл" />
<AppBarButton Click="OnSaveAsAppBarButtonClick"
Icon="Save"
Label="Сохранить файл" />
</StackPanel>
</Grid>
</AppBar>
</Page.BottomAppBar>
</Page>
Обычно часть кнопок строки приложения находится слева, а другая часть - справа. Когда вы держите планшет, такое расположение удобнее, чем кнопки в центре. Распределение кнопок по краям строки может осуществляться средствами XAML. Вероятно, самый простой способ - размещение двух горизонтальных элементов StackPanel в панели Grid с одной ячейкой и их выравнивание по левому и правому краю.
Кнопку New (или Add) рекомендуется размещать в крайней правой позиции. И хотя в нашей программе кнопки New нет, другие кнопки, относящиеся к файлам, также обычно размещаются справа, потому что они имеют отношение к New.
Программные функции находятся слева: кнопки увеличения и уменьшения размера шрифта, еще одна кнопка для включения/выключения переноса. Вот что вы увидите, проведя пальцем у верхнего или нижнего края экрана:
Программа сохраняет настройки пользователя (и содержимое TextBox) в ответ на событие Suspending, определяемое классом Application. Сохраненные настройки загружаются в обработчике Loaded. Для удобства я определил оба обработчика в виде анонимных методов в конструкторе MainPage. Также приведены простые обработчики для кнопок увеличения и уменьшения шрифта:
using System;
using System.Collections.Generic;
using Windows.ApplicationModel;
using Windows.Foundation;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
namespace WinRTTestApp
{
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
// Получение объекта локальных настроек
ApplicationDataContainer appData = ApplicationData.Current.LocalSettings;
Loaded += async (sender, args) =>
{
// Загрузка настроек TextBox
if (appData.Values.ContainsKey("TextWrapping"))
txtbox.TextWrapping = (TextWrapping)appData.Values["TextWrapping"];
if (appData.Values.ContainsKey("FontSize"))
txtbox.FontSize = (double)appData.Values["FontSize"];
// Загрузка содержимого TextBox
StorageFolder localFolder = ApplicationData.Current.LocalFolder;
StorageFile storageFile = await localFolder.CreateFileAsync("AppBarPad.txt",
CreationCollisionOption.OpenIfExists);
txtbox.Text = await FileIO.ReadTextAsync(storageFile);
// Включение TextBox и предоставление фокуса ввода
txtbox.IsEnabled = true;
txtbox.Focus(FocusState.Programmatic);
};
Application.Current.Suspending += async (sender, args) =>
{
// Сохранение настроек TextBox
appData.Values["TextWrapping"] = (int)txtbox.TextWrapping;
appData.Values["FontSize"] = txtbox.FontSize;
// Сохранение содержимого TextBox
SuspendingDeferral deferral = args.SuspendingOperation.GetDeferral();
await PathIO.WriteTextAsync("ms-appdata:///local/AppBarPad.txt", txtbox.Text);
deferral.Complete();
};
}
private void OnFontIncreaseAppBarButtonClick(object sender, RoutedEventArgs args)
{
ChangeFontSize(1.1);
}
private void OnFontDecreaseAppBarButtonClick(object sender, RoutedEventArgs args)
{
ChangeFontSize(1 / 1.1);
}
private void ChangeFontSize(double multiplier)
{
txtbox.FontSize *= multiplier;
}
// ...
}
}
При нажатии кнопки "Перенос текста" программа выводит диалоговое окно с командами "С переносом" и "Без переноса". Я определил макет этого диалогового окна в виде класса WrapOptionsDialog, производного от UserControl. В файле XAML два варианта представлены элементами управления RadioButton:
<UserControl
x:Class="WinRTTestApp.WrapOptionsDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WinRTTestApp.Extensions">
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<StackPanel Name="stackPanel"
Margin="24">
<RadioButton Content="С переносом"
Checked="OnRadioButtonChecked">
<RadioButton.Tag>
<TextWrapping>Wrap</TextWrapping>
</RadioButton.Tag>
</RadioButton>
<RadioButton Content="Без переноса"
Checked="OnRadioButtonChecked">
<RadioButton.Tag>
<TextWrapping>NoWrap</TextWrapping>
</RadioButton.Tag>
</RadioButton>
</StackPanel>
</Grid>
</UserControl>
Стоит заметить, что Grid использует стандартную фоновую кисть. Какая-то кисть нужна, иначе фон будет прозрачным. Я оставил в этой программе темную тему оформления, поэтому диалоговое окно будет иметь белый основной цвет с черным фоном — а следовательно, будет контрастировать с TextBox.
Файл фонового кода для диалогового окна определяет свойство зависимости TextWrapping типа TextWrapping. Обработчик изменения свойства устанавливает состояние RadioButton при задании свойства, а свойство задается при выборе RadioButton пользователем:
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace WinRTTestApp
{
public sealed partial class WrapOptionsDialog : UserControl
{
static WrapOptionsDialog()
{
TextWrappingProperty = DependencyProperty.Register("TextWrapping",
typeof(TextWrapping),
typeof(WrapOptionsDialog),
new PropertyMetadata(TextWrapping.NoWrap, OnTextWrappingChanged));
}
public static DependencyProperty TextWrappingProperty { private set; get; }
public WrapOptionsDialog()
{
this.InitializeComponent();
}
public TextWrapping TextWrapping
{
set { SetValue(TextWrappingProperty, value); }
get { return (TextWrapping)GetValue(TextWrappingProperty); }
}
static void OnTextWrappingChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
(obj as WrapOptionsDialog).OnTextWrappingChanged(args);
}
private void OnTextWrappingChanged(DependencyPropertyChangedEventArgs args)
{
foreach (UIElement child in stackPanel.Children)
{
RadioButton radioButton = child as RadioButton;
radioButton.IsChecked = (TextWrapping)radioButton.Tag == (TextWrapping)args.NewValue;
}
}
private void OnRadioButtonChecked(object sender, RoutedEventArgs args)
{
this.TextWrapping = (TextWrapping)(sender as RadioButton).Tag;
}
}
}
Обработчик события кнопки переноса в строке приложения определяется в файле фонового кода MainPage. Он создает экземпляр объекта WrapOptionsDialog и инициализирует его свойство TextWrapping по свойству TextWrapping поля TextBox. Затем он определяет привязку между двумя свойствами TextWrapping, чтобы результат изменения свойства был виден прямо в TextBox. Затем объект WrapOptionsDialog назначается потомком нового объекта Popup:
public sealed partial class MainPage : Page
{
// ...
private void OnWrapOptionAppBarButtonClick(object sender, RoutedEventArgs args)
{
// Создание диалогового меню
WrapOptionsDialog wrapOptionsDialog = new WrapOptionsDialog
{
TextWrapping = txtbox.TextWrapping
};
// Привязка диалогового окна к TextBox
Binding binding = new Binding
{
Source = wrapOptionsDialog,
Path = new PropertyPath("TextWrapping"),
Mode = BindingMode.TwoWay
};
txtbox.SetBinding(TextBox.TextWrappingProperty, binding);
// Создание всплывающего окна
Popup popup = new Popup
{
Child = wrapOptionsDialog,
IsLightDismissEnabled = true
};
// Настройка положения окна по размерам содержимого
wrapOptionsDialog.Loaded += (dialogSender, dialogArgs) =>
{
// Получение позиции кнопки относительно экрана
Button btn = sender as Button;
Point pt = btn.TransformToVisual(null).TransformPoint(new Point(btn.ActualWidth / 2,
btn.ActualHeight / 2));
popup.HorizontalOffset = pt.X - wrapOptionsDialog.ActualWidth / 2;
popup.VerticalOffset = this.ActualHeight - wrapOptionsDialog.ActualHeight
- this.BottomAppBar.ActualHeight - 48;
};
// Отображение всплывающего окна
popup.IsOpen = true;
}
// ...
}
Обычно такие всплывающие окна располагаются непосредственно над строкой приложения; это означает, что для правильного позиционирования необходимо знать высоту всплывающего окна, высоту страницы и высоту строки приложения. Я также решил позиционировать Popup по горизонтали, чтобы окно было выровнено по вызвавшей его кнопке. Для этого необходимо получить координаты центра кнопки относительно экрана методом TransformToVisual(). Такие вычисления обычно выполняются в обработчике события Loaded или SizeChanged потомка Popup.
Обработчик Click завершается установкой свойства IsOpen объекта Popup. А вот как выглядит результат:
Объект Popup автоматически закрывается, когда пользователь прикасается к экрану за его пределами, после чего пользователь должен еще одним касанием закрыть строку приложения. Так как и AppBar, и Popup поддерживают события Opened и Closed Для выполнения инициализации или очистки, можно назначить обработчик события Closed объекта Popup и использовать его для задания свойству IsOpen объекта AppBar значения false (например).
В логике файлового ввода/вывода используются простые статические методы FileIO, но без обработки исключений:
// ...
private async void OnOpenAppBarButtonClick(object sender, RoutedEventArgs args)
{
FileOpenPicker picker = new FileOpenPicker();
picker.FileTypeFilter.Add(".txt");
StorageFile storageFile = await picker.PickSingleFileAsync();
// Если пользователь нажимает кнопку отмены, возвращается null
if (storageFile == null)
return;
txtbox.Text = await FileIO.ReadTextAsync(storageFile);
}
private async void OnSaveAsAppBarButtonClick(object sender, RoutedEventArgs args)
{
FileSavePicker picker = new FileSavePicker();
picker.DefaultFileExtension = ".txt";
picker.FileTypeChoices.Add("Text", new List<string> { ".txt" });
StorageFile storageFile = await picker.PickSaveFileAsync();
// Если пользователь нажимает кнопку отмены, возвращается null
if (storageFile == null)
return;
await FileIO.WriteTextAsync(storageFile, txtbox.Text);
}
// ...