Редактирование потокового документа

93

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

Инструментальные пакеты для программирования уже давно содержат элементы управления, работающие с форматированным текстом. Однако элемент RichTextControl, включенный в WPF, существенно отличается от его предшественников. Он больше не ограничен устаревшим стандартом RTF, который можно встретить в программах обработки текстов. Вместо этого он хранит свое содержимое в виде объекта FlowDocument.

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

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

Если вам нужно обрабатывать большие объемы данных, применять сложную логику для обработки нажатий клавиш или добавить такие эффекты, как автоматическое форматирование (наподобие подсветки синтаксиса в Visual Studio или подчеркивания при проверке правописания в Word), RichTextBox может и не обеспечить приемлемый уровень производительности.

Элемент RichTextBox не поддерживает все те возможности, которые имеются в контейнерах потоковых документов, доступных только для чтения. К примеру, в нем нет изменения масштаба, разбиения на страницы и колонки, а также поиска.

Загрузка файла

Чтобы опробовать элемент RichTextBox, можно объявить в нем один из уже знакомых вам потоковых документов:

<RichTextBox>
        <FlowDocument>
            <Paragraph>Привет от мира редактируемых документов!</Paragraph>
        </FlowDocument>
</RichTextBox>

Но практичнее прочитать документ из файла, а затем вставить его в RichTextBox. Для этого можно воспользоваться тем же подходом, что и при загрузке и сохранении содержимого элемента FlowDocument перед отображением его в контейнере, доступном только для чтения — статическим методом XamlReader.Load().

Но может понадобиться и еще одна возможность — загрузка и сохранение файлов в других форматах (а именно, .rtf-файлов). Для этой цели нужно использовать класс System.Windows.Documents.TextRange, в который упаковывается часть текста. TextRange — изумительно полезный контейнер, позволяющий преобразовывать файлы из одного формата в другой, а также применять форматирование.

Ниже представлен фрагмент простого кода, который преобразует .rtf-документ в выборку текста в объекте TextRange, после чего вставляет этот текст в RichTextBox:

System.Windows.Forms.OpenFileDialog openFile = 
                new System.Windows.Forms.OpenFileDialog();

openFile.Filter = "RichText files (*.rtf)|*.rtf|All files (*.*)|*.*";
if (openFile.ShowDialog() == true)
{
      TextRange tr = new TextRange(
             richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);

      using (FileStream fs = File.Open(openFile.FileName, FileMode.Open))
      {
             tr.Load(fs, DataFormats.Rtf);
      }
}
Загрузка RTF-документа в WPF

Обратите внимание: прежде чем что-либо сделать, необходимо создать контейнер TextRange для той части документа, которую нужно изменить. Даже если в документе на данный момент нет никакого содержимого, необходимо определить начальную и конечную точки выборки. Чтобы выделить весь документ, можно использовать свойства ContentStart и ContentEnd, которые предоставляют объекты TextPointer, необходимые для контейнера TextRange.

После создания контейнера TextRange его можно заполнить данными с помощью метода Load(). Однако нужно еще указать строку, идентифицирующую тип формата данных, который требуется преобразовать. Можно использовать одно из следующих значений:

Формат DataFormats.XamlPackage практически совпадает с DataFormats.Xaml. Единственное отличие состоит в том, что DataFormats.XamlPackage хранит двоичные данные для внедренных изображений (которые отсутствуют в обычной сериализации DataFormats.Xaml). Формат пакетов XAML не является настоящим стандартом: это просто средство WPF, позволяющее упростить преобразование содержимого документа и поддерживающее другие полезные возможности, вроде вырезания и вставки или перетаскивания.

Класс DataFormats содержит много дополнительных полей, но не поддерживает остальные. Например, невозможно преобразовать документ HTML в потоковое содержимое с помощью DataFormats.Html. И формат XAML, и RTF требуют полномочий на выполнение неуправляемого кода, а это означает, что их нельзя использовать в ситуациях с ограничением доверия (например, в браузерном приложении).

Метод TextRange.Load() работает только в том случае, если указан правильный формат файла. Однако в реальности может понадобиться создать текстовый редактор, поддерживающий как XAML (для большей точности), так и RTF (для совместимости с другими программами, такими как текстовые процессоры). Обычно в таких случаях пользователю дается возможность указать формат файла, или же формат определяется на основе расширения файла, как показано ниже:

using (FileStream fs = File.Open(openFile.FileName, FileMode.Open))
{
       if (Path.GetExtension(openFile.FileName).ToLower() == ".rtf")
       {
              documentTextRange.Load(fs, DataFormats.Rtf);
       }
       else
       {
              documentTextRange.Load(fs, DataFormats.Xaml);
       }
}

В этом коде возникнет исключение, если файл не найден, к нему нет доступа, либо его невозможно загрузить с помощью указанного формата. По всем этим причинам нужно поместить такой код в обработчик исключения.

Помните, что независимо от способа, которым загружается содержимое документа, оно преобразуется в объект FlowDocument, чтобы его можно было отобразить в элементе управления RichTextBox.

Чтобы точнее понять, о чем идет речь, можно написать простую программу, которая извлекает содержимое из объекта FlowDocument и преобразовывает его в текстовую строку с помощью конструкций XamlWriter и TextRange. Ниже показан пример, который выводит разметку для текущего потокового документа в другом текстовом поле:

// Копирование содержимого документа в MemoryStream. 
using (MemoryStream stream = new MemoryStream())
{
                TextRange range = new TextRange(richTextBox.Document.ContentStart,
                    richTextBox.Document.ContentEnd);
                range.Save(stream, DataFormats.Xaml);
                stream.Position = 0;

                // Чтение содержимого из потока и вывод его в текстовом поле. 
                using (StreamReader r = new StreamReader(stream))
                {
                    string line;
                    while ((line = r.ReadLine()) != null)
                        txb_xaml.Text += line + "\n";
                }
} 
Вывод XAML-разметки документа

Этот прием чрезвычайно полезен в качестве средства отладки — он позволяет узнать, как изменяется разметка документа после ее редактирования.

Сохранение файла

Документ можно сохранить с помощью объекта TextRange. Ему надо передать пару объектов TextPointer, определяющих начало и окончание содержимого. Затем можно вызвать метод TextRange.Save() и указать требуемый формат экспорта (текст, XAML, пакет XAML или RTF) с помощью поля из класса DataFormats. Здесь форматы пакета XAML и RTF также требуют полномочий на выполнение неуправляемого кода.

Следующий блок кода сохраняет документ в формате XAML, если имя файла не содержит расширения .rtf. (Другой, более явный подход — дать пользователю возможность применить средство сохранения, использующее XAML, и средство экспортирования, использующее RTF.)

private void save_ButonClick(object sender, RoutedEventArgs e)
{
            Microsoft.Win32.SaveFileDialog save = new Microsoft.Win32.SaveFileDialog();
            save.Filter =
                "Файл XAML (*.xaml)|*.xaml|RTF-файл (*.rtf)|*.rtf";

            if (save.ShowDialog() == true)
            {
                // Создание контейнера TextRange для всего документа
                TextRange documentTextRange = new TextRange(
                    richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);

                // Если такой файл существует, он перезаписывается, 
                using (FileStream fs = File.Create(save.FileName))
                {
                    if (System.IO.Path.GetExtension(save.FileName).ToLower() == ".rtf")
                    {
                        documentTextRange.Save(fs, DataFormats.Rtf);
                    }
                    else
                    {
                        documentTextRange.Save(fs, DataFormats.Xaml);
                    }
                }
            }
}

Если для сохранения документа применяется формат XAML, то естественно можно предположить, что документ сохраняется как обыкновенный файл XAML с элементом верхнего уровня FlowDocument. Почти так, но не совсем. Элементом верхнего уровня должен быть Section.

Как уже было сказано ранее, объект Section представляет собой многоцелевой контейнер, содержащий другие блочные элементы. В этом есть смысл: ведь объект TextRange представляет раздел выделенного содержимого. Однако не пытайтесь использовать метод TextRange.Load() при работе с другими файлами XAML, которые содержат элементы верхнего уровня FlowDocument, Page или Window, поскольку ни один из этих файлов не будет скомпилирован.

Точно так же файл документа не может связаться с файлом с вынесенным кодом или прикрепить какие-либо обработчики событий. Если у вас есть файл XAML, в котором элементом верхнего уровня является FlowDocument, можно создать соответствующий объект FlowDocument с помощью метода XamlReader.Load(), как это уже делалось раньше.

Форматирование выделенного текста

Вы можете многое узнать об элементе RichTextBox, создав простой редактор форматированного текста наподобие приведенного ниже. Здесь кнопки инструментальной панели позволяют пользователю быстро применить жирное, курсивное и подчеркнутое форматирование. Однако наиболее интересной частью этого примера является расположенный ниже обычный элемент TextBox, который выводит разметку XAML для объекта FlowDocument, отображаемого в данный момент в элементе RichTextBox. Это поможет вам понять, как RichTextBox изменяет объект FlowDocument в ходе правок.

Чтобы сделать выделенный текст жирным, курсивным или подчеркнутым, не нужно писать код. Элемент RichTextBox поддерживает команды ToggleBold, ToggleItalic и ToggleUnderline из класса EditingCommands. Кнопки можно связать непосредственно с этими командами. Но данный пример демонстрирует и другие аспекты работы элемента RichTextBox. Это знание пригодится, когда вам понадобится обработать текст другим способом (ниже показан полный код редактора):

<Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="0.5*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <ToolBarTray>
            <ToolBar>
                <Button Click="new_ButonClick">New</Button>
                <Button Click="open_ButonClick">Open</Button>
                <Button Click="save_ButonClick">Save</Button>
            </ToolBar>
            <ToolBar FontFamily="Times New Roman">
                <Button FontWeight="Bold" Command="EditingCommands.ToggleBold">B</Button>
                <Button FontStyle="Italic" Command="EditingCommands.ToggleItalic">I</Button>
                <Button Command="EditingCommands.ToggleUnderline">U</Button>
            </ToolBar>
            <ToolBar>
                <Button Content="Обновить разметку" Click="updatexaml_Click"/>
            </ToolBar>
        </ToolBarTray>
        <TextBlock Margin="10" Text="Загруженный документ:" Grid.Row="1"/>
        <RichTextBox x:Name="richTextBox" Margin="10,0" Grid.Row="2" BorderBrush="LightBlue" BorderThickness="2">
            <FlowDocument>
                <Paragraph>Привет от мира редактируемых документов!</Paragraph>
            </FlowDocument>
        </RichTextBox>
        <GridSplitter Margin="5,10" HorizontalAlignment="Stretch" Height="4" Background="#aaa" Grid.Row="3"/>
        <TextBlock Margin="10,0" Text="XAML-разметка: " Grid.Row="4" MaxHeight="30"/>
        <TextBox x:Name="txb_xaml" Margin="10" TextWrapping="Wrap" Padding="5" Grid.Row="5" BorderBrush="LightBlue" BorderThickness="2"
                 VerticalScrollBarVisibility="Visible"/>
</Grid>
public partial class RichTextEditor : System.Windows.Window
{
        public RichTextEditor()
        {
            InitializeComponent();
        }

        private void save_ButonClick(object sender, RoutedEventArgs e)
        {
            Microsoft.Win32.SaveFileDialog save = new Microsoft.Win32.SaveFileDialog();
            save.Filter =
                "Файл XAML (*.xaml)|*.xaml|RTF-файл (*.rtf)|*.rtf";

            if (save.ShowDialog() == true)
            {
                // Создание контейнера TextRange для всего документа
                TextRange documentTextRange = new TextRange(
                    richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);

                // Если такой файл существует, он перезаписывается, 
                using (FileStream fs = File.Create(save.FileName))
                {
                    if (System.IO.Path.GetExtension(save.FileName).ToLower() == ".rtf")
                    {
                        documentTextRange.Save(fs, DataFormats.Rtf);
                    }
                    else
                    {
                        documentTextRange.Save(fs, DataFormats.Xaml);
                    }
                }
            }
        }

        private void open_ButonClick(object sender, RoutedEventArgs e)
        {
            Microsoft.Win32.OpenFileDialog openFile =
                new Microsoft.Win32.OpenFileDialog();

            openFile.Filter = "RichText files (*.rtf)|*.rtf|All files (*.*)|*.*";
            if (openFile.ShowDialog() == true)
            {
                TextRange tr = new TextRange(
                    richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);

                using (FileStream fs = File.Open(openFile.FileName, FileMode.Open))
                {
                    tr.Load(fs, DataFormats.Rtf);
                }
            }

            // Копирование содержимого документа в MemoryStream. 
            using (MemoryStream stream = new MemoryStream())
            {
                TextRange range = new TextRange(richTextBox.Document.ContentStart,
                    richTextBox.Document.ContentEnd);
                range.Save(stream, DataFormats.Xaml);
                stream.Position = 0;

                // Чтение содержимого из потока и вывод его в текстовом поле. 
                using (StreamReader r = new StreamReader(stream))
                {
                    string line;
                    while ((line = r.ReadLine()) != null)
                        txb_xaml.Text += line + "\n";
                }
            } 
        }

        private void updatexaml_Click(object sender, RoutedEventArgs e)
        {
            TextRange range;

            range = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);

            MemoryStream stream = new MemoryStream();
            range.Save(stream, DataFormats.Xaml);
            stream.Position = 0;

            StreamReader r = new StreamReader(stream);

            txb_xaml.Text = r.ReadToEnd();
            r.Close();
            stream.Close();
        }

        private void new_ButonClick(object sender, RoutedEventArgs e)
        {
            richTextBox.Document = new FlowDocument();
        }
}
Редактирование текста

Элемент RichTextBox содержит еще много других способов обработки текста. Например, классы TextRange и RichTextBox содержат свойства, которые позволяют получать смещения символов, подсчитывать количество строк и переходить от одного потокового элемента к другому в части документа. Для получения дополнительной информации обратитесь к справочной системе Visual Studio.

Получение отдельных слов

У элемента RichTextBox есть один недостаток: он не может выделять отдельные слова в документе. Совсем не трудно найти элемент потокового документа, существующий в данной позиции (как в предыдущем разделе), но единственный способ получить ближайшее слово — перебор символов до ближайшего пробела. Кодировать такой алгоритм утомительно, а внести ошибки легко.

Один из разработчиков WPF — Праджакта Джоши (Prajakta Joshi) — опубликовал неплохое решение для обнаружения промежутков между словами. С помощью этого кода можно быстро создать много интересных эффектов.

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

private void richTextBox_MouseDown(object sender, MouseEventArgs e)
{
     if (e.RightButton == MouseButtonState.Pressed)
     {
           // Получение указателя TextPointer, ближайшего к указателю мыши
           TextPointer location = richTextBox.GetPositionFromPoint
                (Mouse.GetPosition(richTextBox), true);

           // Получение ближайшего слова с помощью этого TextPointer
           TextRange word = WordBreaker.GetWordRange(location);

           txb_FlowDocumentMarkup.Text = word.Text;
      }
}
Пройди тесты
Лучший чат для C# программистов