Элемент RichTextBlock в WinRT

121

Хотя элемент TextBlock остается предпочтительным вариантом для вывода текста, длина которого идет на абзацы, элемент RichTextBlock обладает рядом дополнительных возможностей. Элемент RichTextBlock не содержит свойства Text; у него нет свойства Inlines для задания текста в форме, производной от Inline. Вместо этого RichTextBlock определяет свойство с именем Blocks, которое содержит коллекцию объектов, производных от Block. Как и Inline, класс Block является производным от TextElement, от которого он наследует набор текстовых свойств. Кроме того, Block определяет следующие свойства:

LineHeight
LineStackingStrategy
Margin
TextAlignment

Кроме того, как и Inline, класс Block сам по себе не допускает создание экземпляров. Единственным классом, производным от Block, в настоящее время является класс Paragraph, который определяет два свойства:

Итак, фактически RichTextBlock содержит набор абзацев. Свойство Margin используется для определения расстояния между абзацами, a TextIndent обеспечивает отступ первой строки.

Проект MadTeaParty использует элемент RichTextBlock, вложенный в ScrollViewer, для вывода текста главы 7 «Алисы в Стране Чудес» Льюиса Кэрролла с тремя иллюстрациями Джона Тенниела. Фрагмент файла XAML:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <ScrollViewer Padding="40 20" Width="720">
            <RichTextBlock FontFamily="Cambria" FontSize="24">
                <Paragraph Margin="0 12" TextAlignment="Center" FontSize="40">
                    <Italic>Alice’s Adventures in Wonderland</Italic>
                    <LineBreak/>
                    by
                    <LineBreak/>
                    Lewis Carroll
                </Paragraph>

                <Paragraph Margin="0 24 0 36" TextAlignment="Center" FontSize="30">
                    Chapter VII
                    <LineBreak />
                    A Mad Tea-Party
                </Paragraph>

                <Paragraph Margin="0 6">
                    There was a table set out under a tree in front of the 
                    house, and the March Hare and the Hatter were having tea at
                    it: a Dormouse was sitting between them, fast asleep, and 
                    the other two were using it as a cushion, resting their 
                    elbows on it, and talking over its head. ‘Very uncomfortable 
                    for the Dormouse,’ thought Alice; ‘only, as it’s asleep, I 
                    suppose it doesn’t mind.’
                </Paragraph>

                ...

                <Paragraph Margin="0 6" TextIndent="48">
                    This piece of rudeness was more than Alice could bear: 
                    she got up in great disgust, and walked off; the Dormouse
                    fell asleep instantly, and neither of the others took the 
                    least notice of her going, though she looked back once or
                    twice, half hoping that they would call after her: the last 
                    time she saw them, they were trying to put the Dormouse
                    into the teapot.
                </Paragraph>

                <Paragraph Margin="0 6" TextAlignment="Center">
                    <InlineUIContainer>
                        <Image Source="Images/ChapterVII-3.png" Stretch="None" />
                    </InlineUIContainer>
                </Paragraph>

                <Paragraph Margin="0 6" TextIndent="48">
                    ‘At any rate I’ll never go
                    <Italic>there</Italic> again!’ 
                    said Alice as she picked her way through the wood. ‘It’s 
                    the stupidest tea-party I ever was at in all my life!’
                </Paragraph>
                
                ...
                
        </ScrollViewer>
    </Grid>
</Page>

Класс Paragraph не является производным от FrameworkElement и поэтому не имеет свойства Style. Если вы хотите задать одинаковые свойства для нескольких объектов Paragraph, их придется задать явно. Большинство абзацев в приведенном тексте использует свойство Margin для формирования межабзацных интервалов величиной 12 пикселов и свойство TextIndent для вывода первой строки с отступом 48 пикселов.

Элемент InlineUIContainer не работает с TextBlock, но работает с RichTextBlock. Это позволяет встраивать в текст элементы, производные от UIElement. В частности, к этой категории относится TextBlock, так что этот механизм предоставляет возможность встраивания текстов в абзацы с привязкой к свойству Text.

В программе MadTeaParty элементы Image являются частью RichTextBlock. Для этого они размещаются внутри объекта InlineUIContainer, который должен находиться внутри Paragraph. Не существует средств для «обтекания» изображений текстом абзаца. Если вы хотите реализовать нечто подобное в C# и XAML, придется измерять отдельные слова и позиционировать их самостоятельно.

А вот как выглядит глава после прокрутки к позиции третьего изображения:

Использование элемента RichTextBlock

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

Элемент RichTextBlock реализует событие SelectionChanged и свойство SelectedText для получения выделенного текста (но не его замены или удаления!) и свойства SelectionStart и SelectionEnd. Два последних свойства относятся к типу TextPointer, который не только предоставляет информацию о смещении выделенного фрагмента в TextBlock, но и обозначает позицию выделения в пикселах относительно TextBlock.

Также имеется событие ContextMenuOpening, которое происходит непосредственно перед отображением контекстного меню. Если задать свойству Handled аргументов события значение true, меню не отображается, предоставляя вам возможность вызова собственного контекстного меню.

Если вы предпочитаете запретить возможность выделения текста в RichTextBlock, задайте свойству IsTextSelectionEnabled значение false.

RichTextBlock и переполнение

За время существования письменности использовалось два основных способа представления длинных текстов: свитки с непрерывным текстом (получившие распространение в Древнем Египте, Китае, Греции и Риме) и книги, разделенные на страницы, - форма, которая стала распространяться в Европе в начале нашей эры. Эти два формата также часто встречаются на компьютерах: веб-страницы обычно используют прокрутку, а в ридерах для электронных книг текст разбивается на страницы.

Ранее я показал, что элемент RichTextBlock может выводить прокручиваемый текст, но RichTextBlock значительно превосходит возможности предшествующих текстовых элементов по страничной разбивке документов. Страницы могут отображаться последовательно, как в ридерах, или отображаться параллельно в колонках.

Механизм работает следующим образом: весь текст, который требуется вывести, помещается в элемент RichTextBlock. Этому элементу назначается конечный размер, из-за чего он будет участвовать в вызове Measure (вручную или в составе обычного формирования макета страницы). Если RichTextBlock содержит больше текста, чем может поместиться в выделенном пространстве, свойство HasOverflowContent принимает значение true. Чтобы вывести вторую страницу текста, не поместившуюся в RichTextBlock, создайте экземпляр класса RichTextBlockOverflow и задайте этот экземпляр свойству OverflowContentTarget объекта RichTextBlock.

Класс RichTextBlockOverflow также определяет свойства HasOverflowContent и OverflowContentTarget, которые позволяют создать дополнительные объекты RichTextBlockOverflow для каждой дополнительной страницы и объединить их в цепочку.

Элементы RichTextBlockOverflow наследуют от родительского элемента RichTextBlock все свойства, относящиеся к тексту - FontFamily, FontSize и т. д. Если вы можете оценить максимальное количество страниц, необходимых для отображения документа, всю работу по формированию цепочек можно выполнить в XAML при помощи привязки данных.

Проект YoungGoodmanBrown показывает, как это делается. Текст позаимствован из рассказа Натаниэля Готорна «Молодой Браун», также опубликованного в проекте Гутенберг. Как и в случае с «Алисой в Стране Чудес», я поместил весь текст в один элемент RichTextBlock, но задал свойству OverflowContentTarget этого элемента RichTextBlock элемент RichTextBlockOverflow и так далее по цепочке:

<Page ...>

    <Page.Resources>
        <local:BooleanToVisibilityConverter x:Key="booleanToVisibility" />

        <Style TargetType="RichTextBlock">
            <Setter Property="Width" Value="480" />
            <Setter Property="Margin" Value="24 0 24 0" />
            <Setter Property="FontSize" Value="18" />
            <Setter Property="TextAlignment" Value="Justify" />
        </Style>

        <Style TargetType="RichTextBlockOverflow">
            <Setter Property="Width" Value="480" />
            <Setter Property="Margin" Value="24 0 24 0" />
        </Style>
    </Page.Resources>

    <Grid Background="#FF1D1D1D">
        <ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled">
            <StackPanel Orientation="Horizontal">

                <!-- Текст из книги http://www.gutenberg.org/files/512/512-h/512-h.htm -->

                <RichTextBlock Name="richTextBlock"
                               OverflowContentTarget="{Binding ElementName=overflow1}">
                    <Paragraph TextAlignment="Center">
                        YOUNG GOODMAN BROWN
                    </Paragraph>

                    <Paragraph TextAlignment="Center" Margin="0 12">
                        by
                        <LineBreak />
                        Nathaniel Hawthorne
                    </Paragraph>

                    <Paragraph Margin="0 6">
                        Young Goodman Brown came forth at sunset into the street at Salem
                        village; but put his head back, after crossing the threshold, to
                        exchange a parting kiss with his young wife. And Faith, as the wife was
                        aptly named, thrust her own pretty head into the street, letting the
                        wind play with the pink ribbons of her cap while she called to Goodman
                        Brown.
                    </Paragraph>

                    ...
                    
                </RichTextBlock>

                <RichTextBlockOverflow Name="overflow1"
                           Visibility="{Binding ElementName=richTextBlock,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow2}" />

                <RichTextBlockOverflow Name="overflow2"
                           Visibility="{Binding ElementName=overflow1,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow3}" />

                <RichTextBlockOverflow Name="overflow3"
                           Visibility="{Binding ElementName=overflow2,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow4}" />

                <RichTextBlockOverflow Name="overflow4"
                           Visibility="{Binding ElementName=overflow3,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow5}" />

                <RichTextBlockOverflow Name="overflow5"
                           Visibility="{Binding ElementName=overflow4,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow6}" />

                <RichTextBlockOverflow Name="overflow6"
                           Visibility="{Binding ElementName=overflow5,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow7}" />

                <RichTextBlockOverflow Name="overflow7"
                           Visibility="{Binding ElementName=overflow6,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow8}" />

                <RichTextBlockOverflow Name="overflow8"
                           Visibility="{Binding ElementName=overflow7,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow9}" />

                <RichTextBlockOverflow Name="overflow9"
                           Visibility="{Binding ElementName=overflow8,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow10}" />

                <RichTextBlockOverflow Name="overflow10"
                           Visibility="{Binding ElementName=overflow9,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow11}" />

                <RichTextBlockOverflow Name="overflow11"
                           Visibility="{Binding ElementName=overflow10,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow12}" />

                <RichTextBlockOverflow Name="overflow12"
                           Visibility="{Binding ElementName=overflow11,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow13}" />

                <RichTextBlockOverflow Name="overflow13"
                           Visibility="{Binding ElementName=overflow12,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow14}" />

                <RichTextBlockOverflow Name="overflow14"
                           Visibility="{Binding ElementName=overflow13,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow15}" />

                <RichTextBlockOverflow Name="overflow15"
                           Visibility="{Binding ElementName=overflow14,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow16}" />

                <RichTextBlockOverflow Name="overflow16"
                           Visibility="{Binding ElementName=overflow15,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow17}" />

                <RichTextBlockOverflow Name="overflow17"
                           Visibility="{Binding ElementName=overflow16,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow18}" />

                <RichTextBlockOverflow Name="overflow18"
                           Visibility="{Binding ElementName=overflow17,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow19}" />

                <RichTextBlockOverflow Name="overflow19"
                           Visibility="{Binding ElementName=overflow18,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow20}" />

                <RichTextBlockOverflow Name="overflow20"
                           Visibility="{Binding ElementName=overflow19,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}"
                           OverflowContentTarget="{Binding ElementName=overflow21}" />

                <RichTextBlockOverflow Name="overflow21"
                           Visibility="{Binding ElementName=overflow20,
                                                Path=HasOverflowContent,
                                                Converter={StaticResource booleanToVisibility}}" />
            </StackPanel>
        </ScrollViewer>
    </Grid>
</Page>

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

Переполнение элемента RichTextBlock

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

При создании проекта типа Grid App или Split App в Visual Studio в папке Common создается класс RichTextColumns. Этот класс является производным от Panel и генерирует элементы RichTextBlockOverflow в своем методе MeasureOverride()/

Сейчас я покажу другой способ. Как и в двух предыдущих проектах, файл MainPage.xaml следующего проекта содержит элемент RichTextBlock с полным текстом литературного произведения - на этот раз рассказа Ф. Скотта Фицджеральда «Бернис подстригается»:

<Page ...>

    <Page.Resources>
        <Style TargetType="RichTextBlockOverflow">
            <Setter Property="Width" Value="480" />
            <Setter Property="Margin" Value="24 0 24 0" />
        </Style>
        <Style TargetType="RichTextBlock">
            <Setter Property="Width" Value="480" />
            <Setter Property="Margin" Value="24 0 24 0" />
            <Setter Property="FontSize" Value="18" />
            <Setter Property="TextAlignment" Value="Justify" />
        </Style>
    </Page.Resources>

    <Grid Background="#FF1D1D1D">
        <ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled">
            <StackPanel Name="stackPanel"
                        Orientation="Horizontal">
                <RichTextBlock SizeChanged="RichTextBlock_SizeChanged">
                    <Paragraph TextAlignment="Center" FontSize="36" Margin="0 0 0 12">
                        “Bernice Bobs Her Hair”
                        <LineBreak />
                        by
                        <LineBreak />
                        F. Scott Fitzgerald
                    </Paragraph>

                    <Paragraph Margin="0 6">
                        After dark on Saturday night one could stand on the first tee of
                        the golf-course and see the country-club windows as a yellow
                        expanse over a very black and wavy ocean. The waves of this
                        ocean, so to speak, were the heads of many curious caddies, a few
                        of the more ingenious chauffeurs, the golf professional's deaf
                        sister&amp;#x2014;and there were usually several stray, diffident waves who
                        might have rolled inside had they so desired. This was the
                        gallery.
                    </Paragraph>

                    ...

                    <Paragraph TextIndent="48" Margin="0 6">
                        "Huh," she giggled wildly. "Scalp the selfish thing!"
                    </Paragraph>

                    <Paragraph TextIndent="48" Margin="0 6">
                        Then picking up her staircase she set off at a half-run down the
                        moonlit street.
                    </Paragraph>
                </RichTextBlock>
            </StackPanel>
        </ScrollViewer>
    </Grid>
</Page>

Обратите внимание на обработчик SizeChanged, назначенный элементу RichTextBlock. Выполнение обработчика начинается с удаления всех элементов RichTextBlockOverflow, которые могли быть созданы при предыдущих изменениях размера, а затем создается новая группа:

private void RichTextBlock_SizeChanged(object sender, SizeChangedEventArgs e)
{
    RichTextBlock richTextBlock = sender as RichTextBlock;

    if (richTextBlock.ActualHeight == 0)
        return;

    // Удаление предыдущих объектов RichTextBlockOverflow
    while (stackPanel.Children.Count > 1)
        stackPanel.Children.RemoveAt(1);

    if (!richTextBlock.HasOverflowContent)
        return;

    // Создание первого объекта RichTextBlockOverflow
    RichTextBlockOverflow richTextBlockOverflow = new RichTextBlockOverflow();
    richTextBlock.OverflowContentTarget = richTextBlockOverflow;
    stackPanel.Children.Add(richTextBlockOverflow);

    // Определение размеров
    richTextBlockOverflow.Measure(new Size(richTextBlockOverflow.Width, this.ActualHeight));

    // Если имеется переполняющее содержимое, повторить процесс
    while (richTextBlockOverflow.HasOverflowContent)
    {
        RichTextBlockOverflow newRichTextBlockOverflow = new RichTextBlockOverflow();
        richTextBlockOverflow.OverflowContentTarget = newRichTextBlockOverflow;
        richTextBlockOverflow = newRichTextBlockOverflow;
        stackPanel.Children.Add(richTextBlockOverflow);
        richTextBlockOverflow.Measure(new Size(richTextBlockOverflow.Width, this.ActualHeight));
    }
}

Обратите внимание на вызовы метода Measure объектов RichTextBlock и RichTextBlockOverflow. Они необходимы, чтобы заставить элемент определить, какой объем текста может поместиться в этом прямоугольнике, и задать соответствующее значение свойства HasOverflowContent. Результат:

Прокрутка страницы при переполнении в RichTextBlock
Пройди тесты
Лучший чат для C# программистов