Элемент RichEditBox в WinRT

72

У элемента TextBlock имеется расширенная версия, которая называется RichTextBlock. Точно так же расширенная версия существует и у элемента TextBox, и называется она... нет, не RichTextBox. Она называется RichEditBox.

Если TextBox можно представить как «ядро» традиционной Windows-программы Блокнот (Notepad), элемент RichEditBox может рассматриваться как ядро Windows-программы WordPad. Элемент RichEditBox позволяет на программном уровне выбирать диапазоны текста - или (чаще) предоставлять возможность выделения текста пользователю, чтобы потом применить к выделению средства форматирования символов и абзацев. Элемент RichEditBox также содержит встроенные средства загрузки и сохранения файлов, но, к сожалению, они поддерживают только старомодный формат RTF (Rich Text Format).

Следующее обсуждение дает лишь поверхностное представление о возможностях RichEditBox. Изучение уникальных возможностей этого класса стоит начать со свойства Document. Этому свойству во внутренней реализации задается объект, реализующий интерфейс ITextDocument из пространства имен Windows.UI.Text. Указанный интерфейс поддерживает загрузку и сохранение в потоки данных, а также предоставляет методы для задания и получения форматирования символов и абзацев по умолчанию и назначения форматирования для диапазонов текста в документе.

Интерфейс ITextDocument также поддерживает свойство Selection, которое обозначает выделенную пользователем область документа. Свойство Selection относится к типу ITextSelection, также реализующему интерфейс ITextRange. Интерфейс ITextRange поддерживает операции копирования и вставки из буфера, а также определение свойств CharacterFormat и ParagraphFormat, которые ссылаются на объекты, реализующие интерфейсы ITextCharacterFormat и ITextParagraphFormat соответственно.

Давайте воспользуемся элементом RichEditBox для построения простейшего редактора с форматированием текста, который будет называться RichTextEditor. В верхней части программы располагается строка приложения со средствами форматирования символов (слева) и абзацев (справа), а в нижней части - строка приложения для загрузки и сохранения файлов.

Программа редактирования текста с помощью RichEditBox

Стоит взяться за построение программ такого рода, и вы очень быстро поймете, что основные сложности связаны вовсе не с интерфейсом программирования RichEditBox, а с организацией пользовательского интерфейса. Средства выбора размера и семейства шрифта реализованы как элементы управления ComboBox прямо в строке приложения, а возможность выбора цвета отсутствует. Как я уже говорил, это приложение дает лишь общее представление о работе с RichEditBox.

Ниже приведен файл XAML. Как видите, разметка RichEditBox занимает совсем немного места по сравнению с двумя определениями AppBar:

<Page
    x:Class="WinRTTestApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinRTTestApp">

    <Grid Background="#FF1D1D1D">
        <RichEditBox Name="richEditBox" />
    </Grid>

    <Page.TopAppBar>
        <AppBar Opened="AppBar_Opened">
            <Grid>
                <StackPanel Orientation="Horizontal"
                            HorizontalAlignment="Left">
                    <AppBarToggleButton Name="boldAppBarCheckBox"
                                        Checked="boldAppBarCheckBox_Checked"
                                        Unchecked="boldAppBarCheckBox_Checked"
                                        Icon="Bold" Label="Жирный"/>

                    <AppBarToggleButton Name="italicAppBarCheckBox"
                                        Checked="italicAppBarCheckBox_Checked"
                                        Unchecked="italicAppBarCheckBox_Checked"
                                        Icon="Italic" Label="Курсив"/>

                    <AppBarToggleButton Name="underlineAppBarCheckBox"
                                        Checked="underlineAppBarCheckBox_Checked"
                                        Unchecked="underlineAppBarCheckBox_Checked"
                                        Icon="Underline"  Label="Подчеркнутый"/>

                    <ComboBox Name="fontSizeComboBox"
                              Width="72"
                              Margin="12 12 24 36"
                              SelectionChanged="fontSizeComboBox_SelectionChanged">
                        <x:Int32>8</x:Int32>
                        <x:Int32>9</x:Int32>
                        <x:Int32>10</x:Int32>
                        <x:Int32>11</x:Int32>
                        <x:Int32>12</x:Int32>
                        <x:Int32>14</x:Int32>
                        <x:Int32>16</x:Int32>
                        <x:Int32>18</x:Int32>
                        <x:Int32>20</x:Int32>
                        <x:Int32>22</x:Int32>
                        <x:Int32>24</x:Int32>
                        <x:Int32>26</x:Int32>
                        <x:Int32>28</x:Int32>
                        <x:Int32>36</x:Int32>
                        <x:Int32>48</x:Int32>
                        <x:Int32>72</x:Int32>
                    </ComboBox>

                    <ComboBox Name="fontFamilyComboBox" 
                              Width="240"
                              Margin="12 12 24 36"
                              SelectionChanged="fontFamilyComboBox_SelectionChanged" />
                </StackPanel>
                <StackPanel Orientation="Horizontal"
                            HorizontalAlignment="Right">

                    <StackPanel Name="alignmentPanel"
                                Orientation="Horizontal">
                        <AppBarButton Name="alignLeftAppBarRadioButton"
                                      Click="alignAppBarRadioButton_Click"
                                      Icon="AlignLeft" Label="Выравнивание слева"/>

                        <AppBarButton Name="alignCenterAppBarRadioButton"
                                      Click="alignAppBarRadioButton_Click"
                                      Icon="AlignCenter" Label="Выравнивание по центру"/>

                        <AppBarButton Name="alignRightAppBarRadioButton"
                                      Click="alignAppBarRadioButton_Click"
                                      Icon="AlignRight" Label="Выравнивание справа"/>
                    </StackPanel>
                </StackPanel>
            </Grid>
        </AppBar>
    </Page.TopAppBar>
    <Page.BottomAppBar>
        <AppBar>
            <Grid>
                <StackPanel Orientation="Horizontal"
                            HorizontalAlignment="Left" />

                <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
                    <AppBarButton Click="open_Click" Icon="OpenFile" Label="Открыть файл" />

                    <AppBarButton Icon="SaveLocal" Click="save_Click" Label="Сохранить как" />
                </StackPanel>
            </Grid>
        </AppBar>
    </Page.BottomAppBar>
</Page>

Элемент управления AppBarToggleButton используется для трех кнопок, включающих и отключающих полужирное (Bold) или курсивное (Italic) начертание, а также подчеркивание (Underline). Поле ComboBox для размера шрифта инициализируется явно заданными значениями. Обычно текстовые редакторы поддерживают ввод значений, выбранных пользователем, но для простоты я отказался от этой возможности. Список ComboBox для семейства шрифтов инициализируется в файле фонового кода. Три кнопки выравнивания текста составляют группу элементов управления RadioButton.

Поскольку в этом проекте второй элемент управления ComboBox должен заполняться списком шрифтов, установленных в системе, в него включается проект DirectXWrapper из статьи DirectWrite и шрифты в WinRT. Содержимое ComboBox инициализируется в обработчике Loaded. Обработчик Loaded также загружает из локального хранилища текущий документ и два параметра, определяющих последний выделенный диапазон.

Документ и параметры сохраняются в обработчике Suspending:

using DirectXWrapper;
using System;
using System.Collections.Generic;
using Windows.ApplicationModel;
using Windows.Foundation.Collections;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
using Windows.UI.Popups;
using Windows.UI.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            Loaded += OnLoaded;
            Application.Current.Suspending += OnAppSuspending;
        }

        private async void OnLoaded(object sender, RoutedEventArgs e)
        {
            // Получение списка шрифтов от библиотеки DirectXWrapper
            WriteFactory writeFactory = new WriteFactory();
            WriteFontCollection writeFontCollection =
                        writeFactory.GetSystemFontCollection();

            int count = writeFontCollection.GetFontFamilyCount();
            string[] fonts = new string[count];

            for (int i = 0; i < count; i++)
            {
                WriteFontFamily writeFontFamily =
                                    writeFontCollection.GetFontFamily(i);

                WriteLocalizedStrings writeLocalizedStrings =
                                    writeFontFamily.GetFamilyNames();
                int index;

                if (writeLocalizedStrings.FindLocaleName("en-us", out index))
                    fonts[i] = writeLocalizedStrings.GetString(index);
                else
                    fonts[i] = writeLocalizedStrings.GetString(0);
            }

            Array.Sort<string>(fonts);
            fontFamilyComboBox.ItemsSource = fonts;

            // Загрузка текущего документа
            StorageFolder localFolder = ApplicationData.Current.LocalFolder;

            try
            {
                StorageFile storageFile = await localFolder.CreateFileAsync("RichTextEditor.rtf",
                                                       CreationCollisionOption.OpenIfExists);
                IRandomAccessStream stream = await storageFile.OpenAsync(FileAccessMode.Read);
                richEditBox.Document.LoadFromStream(TextSetOptions.FormatRtf, stream);
            }
            catch
            {
                // Исключения игнорируются
            }

            // Загрузка параметров выделения
            IPropertySet propertySet = ApplicationData.Current.LocalSettings.Values;

            if (propertySet.ContainsKey("SelectionStart"))
                richEditBox.Document.Selection.StartPosition = (int)propertySet["SelectionStart"];

            if (propertySet.ContainsKey("SelectionEnd"))
                richEditBox.Document.Selection.EndPosition = (int)propertySet["SelectionEnd"];
        }

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

            // Сохранение текущего документа
            StorageFolder localFolder = ApplicationData.Current.LocalFolder;

            try
            {
                StorageFile storageFile = await localFolder.CreateFileAsync("RichTextEditor.rtf",
                                                       CreationCollisionOption.ReplaceExisting);
                IRandomAccessStream stream = await storageFile.OpenAsync(FileAccessMode.ReadWrite);
                richEditBox.Document.SaveToStream(TextGetOptions.FormatRtf, stream);
            }
            catch
            {
                // Исключения игнорируются
            }

            // Сохранение параметров выделения
            IPropertySet propertySet = ApplicationData.Current.LocalSettings.Values;
            propertySet["SelectionStart"] = richEditBox.Document.Selection.StartPosition;
            propertySet["SelectionEnd"] = richEditBox.Document.Selection.EndPosition;

            deferral.Complete();
        }
        
        // ...

        
    }
}

Для загрузки и сохранения файлов RTF методы LoadFromStream() и SaveToStream(), определенные интерфейсом ITextDocument, должны получать перечисляемое значение FormatRtf. В противном случае методы загружают и сохраняют обычный текст.

Обработчик Suspending сохраняет только свойства StartPosition и EndPosition объекта ITextSelection, хранящегося в свойстве Selection объекта ITextDocument. Если выделенный текст отсутствует, значения совпадают и обозначают текущую позицию курсора в документе - то есть позицию, в которой будет вставляться вводимый текст.

Программа не сохраняет информации о форматировании, потому что она не поддерживает форматирование по умолчанию, применимое к новым документам или файлам с обычным текстом. Средства форматирования в строке приложения применяются только к конкретному выделению (или точке вставки) в документе. Все эти спецификации форматирования являются внутренними по отношению к документу, который поддерживает RichEditBox. (Конечно, программа может предоставить возможность выбора форматирования по умолчанию для всего документа, но это создаст еще одну проблему из области пользовательского интерфейса.)

Так как программа не сохраняет информацию о форматировании, она должна инициализировать все средства форматирования текста в верхней строке приложения при выводе строки приложения, о котором сигнализирует событие Opened. Для инициализации используется текущее выделение или позиция вставки в документе. Текущие настройки хранятся в свойствах CharacterFormat и ParagraphFormat объекта ITextSelection, хранящегося в свойстве Selection объекта ITextDocument:

private void AppBar_Opened(object sender, object e)
{
    // Получение форматирования символов для текущего выделения
    ITextCharacterFormat charFormat = richEditBox.Document.Selection.CharacterFormat;

    // Задание состояния кнопок AppBarToggleButton в строке приложения
    boldAppBarCheckBox.IsChecked = charFormat.Bold == FormatEffect.On;
    italicAppBarCheckBox.IsChecked = charFormat.Italic == FormatEffect.On;
    underlineAppBarCheckBox.IsChecked = charFormat.Underline == UnderlineType.Single;

    // Заполнение двух полей ComboBox's
    fontSizeComboBox.SelectedItem = (int)charFormat.Size;
    fontFamilyComboBox.SelectedItem = charFormat.Name;
}

Объект ITextCharacterFormat определяет свойства Bold, Italic и Underline (как видно из примера), но они также дополняются знакомым свойством FontStyle и свойством Weight с числовым значением, соответствующим свойствам класса FontWeights.

FormatEffect - перечисление со значениями On, Off, Toggle и Undefined. Если в текущем выделении содержится как курсивный, так и обычный текст, свойство Italic принимает значение FormatEffect.Undefined, а соответствующая кнопка строки приложения должна, вероятно, находиться в неопределенном состоянии, но со стандартным стилем строки приложения это состояние не отличается от выключенного, поэтому я не стал с ним возиться.

Семейство шрифтов, применимое к выделению, предоставляется строковым свойством Name объекта ITextCharacterFormat. Кнопки Bold, Italic и Underline обрабатываются аналогичным образом. Свойства Bold, Italic и Underline объекта ITextCharacterFormat задаются на основании состояния AppBarToggleButton, поэтому эти значения относятся к текущему выделению или позиции вставки:

private void boldAppBarCheckBox_Checked(object sender, RoutedEventArgs e)
{
    richEditBox.Document.Selection.CharacterFormat.Bold =
        (sender as AppBarToggleButton).IsChecked.Value ? FormatEffect.On : FormatEffect.Off;
}

private void italicAppBarCheckBox_Checked(object sender, RoutedEventArgs e)
{
    richEditBox.Document.Selection.CharacterFormat.Italic =
        (sender as AppBarToggleButton).IsChecked.Value ? FormatEffect.On : FormatEffect.Off;
}

private void underlineAppBarCheckBox_Checked(object sender, RoutedEventArgs e)
{
    richEditBox.Document.Selection.CharacterFormat.Underline =
        (sender as AppBarToggleButton).IsChecked.Value ? UnderlineType.Single : UnderlineType.None;
}

Обработчики двух элементов управления ComboBox выглядят почти так же просто:

private void fontSizeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ComboBox comboBox = sender as ComboBox;
    if (comboBox.SelectedItem != null)
    {
        richEditBox.Document.Selection.CharacterFormat.Size = (int)comboBox.SelectedItem;
    }
}

private void fontFamilyComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ComboBox comboBox = sender as ComboBox;
    if (comboBox.SelectedItem != null)
    {
        richEditBox.Document.Selection.CharacterFormat.Name = (string)comboBox.SelectedItem;
    }
}

Последний инструмент форматирования в строке приложения относится к абзацам. Свойству Alignment объекта ITextParagraphFormat задается один из членов перечисления ParagraphAlignment в зависимости от установленного переключателя RadioButton:

private void alignAppBarRadioButton_Click(object sender, RoutedEventArgs e)
{
    ParagraphAlignment paragraphAlign = ParagraphAlignment.Undefined;

    if (sender == alignLeftAppBarRadioButton)
        paragraphAlign = ParagraphAlignment.Left;

    else if (sender == alignCenterAppBarRadioButton)
        paragraphAlign = ParagraphAlignment.Center;

    else if (sender == alignRightAppBarRadioButton)
        paragraphAlign = ParagraphAlignment.Right;

    richEditBox.Document.Selection.ParagraphFormat.Alignment = paragraphAlign;
}

private async void open_Click(object sender, RoutedEventArgs e)
{
    FileOpenPicker picker = new FileOpenPicker();
    picker.FileTypeFilter.Add(".txt");
    picker.FileTypeFilter.Add(".rtf");
    StorageFile storageFile = await picker.PickSingleFileAsync();

    // If user presses Cancel, result is null
    if (storageFile == null)
        return;

    TextSetOptions textOptions = TextSetOptions.None;

    if (storageFile.ContentType != "text/plain")
        textOptions = TextSetOptions.FormatRtf;

    string message = null;

    try
    {
        IRandomAccessStream stream = await storageFile.OpenAsync(FileAccessMode.Read);
        richEditBox.Document.LoadFromStream(textOptions, stream);
    }
    catch (Exception exc)
    {
        message = exc.Message;
    }

    if (message != null)
    {
        MessageDialog msgdlg = new MessageDialog("Файл не может быть открыт. " +
                         "Windows сообщила об ошибке: " +
                         message, "RichTextEditor");
        await msgdlg.ShowAsync();
    }
}

private async void save_Click(object sender, RoutedEventArgs e)
{
    FileSavePicker picker = new FileSavePicker();
    picker.DefaultFileExtension = ".rtf";
    picker.FileTypeChoices.Add("Документ RTF", new List<string> { ".rtf" });
    picker.FileTypeChoices.Add("Текстовый документ", new List<string> { ".txt" });
    StorageFile storageFile = await picker.PickSaveFileAsync();

    if (storageFile == null)
        return;

    TextGetOptions textOptions = TextGetOptions.None;

    if (storageFile.ContentType != "text/plain")
        textOptions = TextGetOptions.FormatRtf;

    string message = null;

    try
    {
        IRandomAccessStream stream = await storageFile.OpenAsync(FileAccessMode.ReadWrite);
        richEditBox.Document.SaveToStream(textOptions, stream);
    }
    catch (Exception exc)
    {
        message = exc.Message;
    }

    if (message != null)
    {
        MessageDialog msgdlg = new MessageDialog("Файл не может быть сохранен. " +
                         "Windows сообщила об ошибке: " +
                         message, "RichTextEditor");
        await msgdlg.ShowAsync();
    }
}
Пройди тесты
Лучший чат для C# программистов