Строка приложения для текстового редактора в WinRT

131

В статье «Файловый ввод/вывод в 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);
}
          
// ...
Пройди тесты
Лучший чат для C# программистов