Относительно позиционированные элементы в документах WPF

    Пару месяцев назад мне пришлось реализовывать интерфейс с помощью WPF. В основном использовался FlowDocument, т.к. необходимо было максимально близко организовать UI в стиле веб-страниц.
    Привыкший к свободе по позиционированию HTML-элементов с помощью CSS, я не мог найти решение по относительному позиционированию вложенных элементов. Свойства Top, Left, Right, Bottom полностью отсутствуют в плавающих WPF документах. MSDN выдал только класс Figure. Однако HorizontalOffset и VerticalOffset не работают при использовании FlowDocumentScrollViewer. Поиск в гугле также не помог.
    Однако решение оказалось более чем простым.

    Как работают браузерные движки

    Наиболее интересной информацией о своей внутренней реализации предоставляет WebKit. Блог представляет собой ценнейший источник информации для тех, кто не хочет изучать исходный код проекта, но при этом хочет знать об его архитектуре.

    Особый интерес вызвал цикл постов о WebCore Rendering. На этой странице и нашлась главная фраза, которая и натолкнула на решение проблемы: «Relative positioning is literally nothing more than a paint-time translation», что на русском: «Относительное позиционирование в буквальном смысле не более чем перемещение при прорисовке».

    Вооружившись данным «постулатом», я решил найти что-либо похожее в недрах WPF.

    TextEffect

    Если для UIElement мы обычно используем RenderTrasform и 2D-трансформации вида Translate, Rotate и Scale, тот же механизм предусмотрен для объектов содержащих текст. Контейнером всех преобразований служит класс TextEffect.

    Итак, приступим к реализации. В качестве примера будет использоваться пример со страницы блога.


    <div style="border:5px solid black; padding:20px; width:300px; margin-left:auto; margin-right:auto">
    Here is a line of text. <span style="position:relative;top:-10px; background-color:#eeeeee">This part is shifted<br> up a bit</span>, but the rest of the line is in its original position.
    </div>
    

    Тот же пример, но только для WPF, без позиционирования:


    <FlowDocumentScrollViewer>
      <FlowDocumentScrollViewer.Document>
        <FlowDocument Name="doc" FontFamily="Verdana" FontSize="12px" LineHeight="18px">
          <Paragraph Padding="20px" BorderThickness="5px" BorderBrush="Black">
            Here is a line of text.
            <Span Name="shiftedText" Background="#EEE">This part is shifted<LineBreak/>
              up a bit
            </Span>, but the rest of the line is in its original position.
          </Paragraph>
        </FlowDocument>
      </FlowDocumentScrollViewer.Document>
    </FlowDocumentScrollViewer>
    

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

    Применим эффект:

    void DoEffect()
    {
      foreach (var run in shiftedText.Inlines.OfType<Run>())
      {
        TextEffect f = new TextEffect();
        TranslateTransform t = new TranslateTransform(0, -10d);
        f.Transform = t;
        int selectionStart = doc.ContentStart.GetOffsetToPosition(run.ElementStart);
        int selectionLength = run.ElementStart.GetOffsetToPosition(run.ElementEnd);
        f.PositionStart = selectionStart;
        f.PositionCount = selectionLength;
        run.TextEffects.Add(f);
      }
    }
    

    И код для отмены:

    void UndoEffect()
    {
      foreach (var run in shiftedText.Inlines.OfType<Run>())
      {
        run.TextEffects.Clear();
      }
    }
    

    Таким образом, мы смогли получить желаемый эффект. Надеюсь, что код пригодится и другим.
    Ссылка на проект с примером.
    Share post

    Comments 19

      0
      При чем здесь относительно позиционированные элементы?
        0
        я имел ввиду элементы, у которых position: relative.
          0
          Не нашел никаких элементов, кроме FlowDocumentViewer.
            0
            я имею ввиду элементы самого FlowDocument типа Span, Run, Paragrapgh и т.п.
            position: relative — это из CSS.
            и, вообще, не FlowDocumentViewer, а FlowDocumentReader; причем существуют еще и FlowDocumentPageViewer и RichTextBox, вместе с FlowDocumentScrollViewer.
              0
              >>Span, Run, Paragrapgh
              Таковые элементами не являются

              >>и, вообще, не FlowDocumentViewer, а FlowDocumentReader; причем существуют еще и FlowDocumentPageViewer и RichTextBox, вместе с FlowDocumentScrollViewer.
              Хотел написать FlowDocumentScrollViewer, спасибо кэп.
                0
                >>Span, Run, Paragraph
                >>Таковыми не являются
                выходит между нами недопонимание.
                цитата из MSDN
                Элементы Paragraph, List, ListItem и Bold используются для управления форматированием содержимого в зависимости от их порядка в разметке. Например, элемент Bold охватывает только часть текста в абзаце; в результате только эта часть текста будет выделена жирным шрифтом. Пользователям HTML это поведение будет знакомо.
                  0
                  Элементом в WPF может быть только наследник UIElement, т.к. только данный класс содержит методы Measure()/MeasureCore() и Arrange()/ArrangeCore() (ну и Desired, RenderSize; ActualHeight и Width, и т.д.). Без них смысла в относительной компоновке нет (да и банально компилятор не даст, т.к. коллекция Panel.Children типа UIElementCollection).

                  А блочные элементы элементами, в классическом смысле этого слова, не являются.

                  Делайте выводы.
                    0
                    Вы имеет ввиду элементы UI — контролы. в данном случае Measure()/MeasureCore() и Arrange()/ArrangeCore() и необходимы layout manger'у WPF, чтобы позиционировать контролы в окне и родительском элементе.
                    Элементом UI в WPF могут быть только классы, наследуемые от DependencyObject, а т.к. inline-элементы (Run, Span, Bold и т.д.) и block-элементы (Paragraph, Section и т.д.) также наследники DependencyObject, следовательно, они также элементы WPF.
                    Относительную компоновку я имел ввиду внутри FlowDocument, но не обычного окна.

                    P.S.
                    Run обладает отличительным свойством: Вы можете его использовать вместе UIElement.
                      0
                      >>Элементом UI в WPF могут быть только классы, наследуемые от DependencyObject, а т.к. inline-элементы (Run, Span, Bold и т.д.) и block-элементы (Paragraph, Section и т.д.) также наследники DependencyObject, следовательно, они также элементы WPF.
                      Классы, наследуемые от DependencyObject являются «объектами зависимостей», позволяющий реализовывать такие вещи, как свойства зависимостей и маршрутизируемые события. Данные вещи, конечно, больше применимы к элементам, но ничто не мешает использовать их в какой-нибудь сторонней логике (например привязка данных, хотя для этого лучше реализовывать INotifyPropertyChanged).

                      Так к чему все это. Классы, наследуемые от DependencyObject, не являются элементами. По вашей логике DispatcherObject также будет являться элементом.

                      >>Относительную компоновку я имел ввиду внутри FlowDocument, но не обычного окна.
                      Уместнее было бы «потоковая компоновка».

                      >>Run обладает отличительным свойством: Вы можете его использовать вместе UIElement.
                      В данном случае таким свойством обладают все классы, наследуемые от Object, а т.е. все. С таким же успехом можно использовать <sys:String>меня можно прочитать</sys:String>, и текст будет читаем.
                        0
                        Классы, наследуемые от DependencyObject являются «объектами зависимостей», позволяющий реализовывать такие вещи, как свойства зависимостей и маршрутизируемые события. Данные вещи, конечно, больше применимы к элементам, но ничто не мешает использовать их в какой-нибудь сторонней логике (например привязка данных, хотя для этого лучше реализовывать INotifyPropertyChanged).

                        Спасибо, знал и без Вас.

                        Так к чему все это. Классы, наследуемые от DependencyObject, не являются элементами. По вашей логике DispatcherObject также будет являться элементом.

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

                        во-первых, если посмотреть на все унаследованные классы от DependencyObject, то получится, что не существует ни одного элемента UI, котрый бы стоял особняком.

                        во-вторых, если необходимо создать свой собственый контрол, от какого базового класса Вы будете наследовать? правильно, либо от UIElement, либо от ContentElement.

                        в-третьих, заранее прошу прощения, Вы вообще представляете, почему WPF оперирует классами, унаследованными от DispatcherObject?

                        >>Run обладает отличительным свойством: Вы можете его использовать вместе UIElement.
                        В данном случае таким свойством обладают все классы, наследуемые от Object…

                        Ну это просто XamlReader вызывает метод ToString() у любого класса, ему незнакомого для построения UI.
                        >>Run обладает отличительным свойством: Вы можете его использовать вместе UIElement.
                        С таким же успехом можно использовать <sys:String>меня можно прочитать</sys:String>, и текст будет читаем.

                        Имелось ввиду то обстоятельтсво, что не каждый класс из System.Windows.Documents можно смело использовать как контент для элементов UIElement.

                        Уместнее было бы «потоковая компоновка».

                        «потоковая компоновка» — она же Flow Layout. Собственно об этом и речь, но с одной оговоркой: пост посвящен тому, чтобы использовать подобно FixedDocument, realtive positioning.
                          0
                          >>в-нулевых, не по моей логике, т.к. я указал одну тонкую вещь: класс DependencyObject, но не DispatcherObject. Все-таки DispatcherObject живет и сам по себе.
                          Ясное дело. В данном случае это был пример — DispatcherObject является элементом не более чем DependencyObject, а т.е. вообще не является таковым.

                          >>во-вторых, если необходимо создать свой собственый контрол, от какого базового класса Вы будете наследовать? правильно, либо от UIElement, либо от ContentElement.
                          >>Элементом UI в WPF могут быть только классы, наследуемые от DependencyObject
                          Вы противоречите сами себе, вижу дальше смысла разъяснять мне что-либо нет, но все же я продолжу…

                          >>в-третьих, заранее прошу прощения, Вы вообще представляете, почему WPF оперирует классами, унаследованными от DispatcherObject?
                          Еще как, но, казалось бы, при чем здесь это?

                          >>Ну это просто XamlReader вызывает метод ToString() у любого класса, ему незнакомого для построения UI.
                          Процитирую вас
                          Спасибо, знал и без Вас.


                          >>Имелось ввиду то обстоятельтсво, что не каждый класс из System.Windows.Documents можно смело использовать как контент для элементов UIElement.
                          Конечно. А еще можно использовать любой класс, переопределяющий ToString(), а таких — тысячи.
                            0
                            >>Элементом UI в WPF могут быть только классы, наследуемые от DependencyObject
                            Вы противоречите сами себе, вижу дальше смысла разъяснять мне что-либо нет, но все же я продолжу…

                            неужели?, ведь:
                            if (typeOf(UIElement) is DependencyObject && typeOf(ContentElement) is DependencyObject)
                            {
                              try
                              {
                                /*любой UI-элемент, который Вы создадите, будет базироваться на вышеперечисленных
                                * т.к. будет использоваться PresentationFramework
                                * который в свою очередь построен вокруг DependencyObject
                                */
                              }
                              catch (Exception) { }
                              finally
                              {
                                //надеюсь моя логика теперь понятна?
                              }
                            }

                            >>в-третьих, заранее прошу прощения, Вы вообще представляете, почему WPF оперирует классами, унаследованными от DispatcherObject?
                            Еще как, но, казалось бы, при чем здесь это?

                            это я для того интересуюсь, чтобы узнать есть ли у Вас представление о WPF, включая не только namespace System.Windows.Controls.
                            ведь, Вы с самого начала полезли в дебри UIElement, привели пример Measure()/MeasureCore() и Arrange()/ArrangeCore().
                            для чего это? обсуждение же касалось именно FlowDocument и элементов внутри них. здесь невозможно относительное позиционирование относительно (извиняюсь за тавтологию) предшествующих и следующих элементов в общем потоке.
                            поэтому и в посте приводится решение данной проблемы.
                            и также цитируя Вас:
                            >>вижу дальше смысла разъяснять мне что-либо нет
                              0
                              >>неужели?, ведь:
                              И что вы мне хотели эти показать? Что DependencyObject является предком UIElement'а? Достаточно было референса из MSDN, а не показывать это все защитным блоком (если объясните при чем здесь он вообще, получите печеньку), да впрочем иерархию наследования и без вас знал. Только от этого DependecyObject элементом не стал, ровно как и ContentElement и его наследники.

                              >>это я для того интересуюсь, чтобы узнать есть ли у Вас представление о WPF, включая не только namespace System.Windows.Controls.
                              ведь, Вы с самого начала полезли в дебри UIElement, привели пример Measure()/MeasureCore() и Arrange()/ArrangeCore().
                              Вы меня пугаете! Вторая часть предложения меня просто отправила в нокаут.
                              Ладно, вижу случай тяжелый, попробую более простым путем:
                              Вот странно, чем же тогда вообще отличаются элементы (имеется в виду UIElement, если чо), например, от Visual, или вообще от, как вы пытаетесь мне вбить, DependecyObject?

                              >>для чего это? обсуждение же касалось именно FlowDocument и элементов внутри них. здесь невозможно относительное позиционирование относительно (извиняюсь за тавтологию) предшествующих и следующих элементов в общем потоке.
                              поэтому и в посте приводится решение данной проблемы.
                              Опущу объяснение первого предложения (а написан там бред), т.к. толку все равно будет мало.
                              А вот насчет последнего скажу вам, что трансформации рендеринга (а в TextEffect можно применять только такие) не имеют ничего общего с относительной компоновкой.
                                0
                                вижу недопонимание еще больше, чем ожидалось. ну что ж начну с чистого листа.
                                Элементом в WPF может быть только наследник UIElement, т.к. только данный класс содержит методы Measure()/MeasureCore() и Arrange()/ArrangeCore() (ну и Desired, RenderSize; ActualHeight и Width, и т.д.). Без них смысла в относительной компоновке нет (да и банально компилятор не даст, т.к. коллекция Panel.Children типа UIElementCollection).

                                Элементом управления UI, элементом UI.
                                А блочные элементы элементами, в классическом смысле этого слова, не являются.

                                ну, это и так понятно, я имел ввиду элементы как таковые.
                                MSDN:
                                Элементы Paragraph, List, ListItem и Bold используются для управления форматированием содержимого в зависимости от их порядка в разметке. Например, элемент Bold охватывает только часть текста в абзаце; в результате только эта часть текста будет выделена жирным шрифтом. Пользователям HTML это поведение будет знакомо.

                                >>Относительную компоновку я имел ввиду внутри FlowDocument, но не обычного окна.
                                Уместнее было бы «потоковая компоновка».

                                имелся ввиду именно класс FlowDocument, но не компоновка layout manager'a как таковая.
                                ...DispatcherObject является элементом не более чем DependencyObject, а т.е. вообще не является таковым.

                                полностью согласен. просто к чему я привел пример DependencyObject? к тому, что весь
                                1. PresentationFramework построен вокруг данного класса
                                2. при создании собственного элемента (не только управления) будет наследоваться базовый класс либо UIElement, либо ContentElement.
                                3. и Visual, и Brush и т.д. не являются элементами — это и так понятно. см. предыдущий пункт.

                                обсуждение же касалось именно FlowDocument и элементов внутри них. здесь невозможно относительное позиционирование относительно (извиняюсь за тавтологию) предшествующих и следующих элементов в общем потоке.

                                уточню еще больше — относительно своего уже расчитанного места. возможно все недопонимание идет от того, что каждый из нас понимает под словами относительное позиционирование, FlowDocument, элемент.
                                и
                                >>если объясните при чем здесь он вообщечто тут непонятно, получите печеньку.
                                  0
                                  >>Элементом управления UI, элементом UI.
                                  Под элементами обычно (я бы даже сказал всегда, но ваши комментарии заставали меня усомниться в этом) подразумеваются, как вы называете, «элементы управления UI». Я не понимаю, с какого потолка вы взяли «элементы UI», но с, например, кнопкой ContentElement и его наследники ничего общего не имеют.

                                  >>ну, это и так понятно, я имел ввиду элементы как таковые.
                                  Как же вы не поймете, что под элементами в данном случае подразумевались элементы XML разметки (в данном случае XAML). Я заметил, что вы любите проводить аналогии с CSS, то должны лучше меня знать про это.

                                  >>полностью согласен. просто к чему я привел пример DependencyObject? к тому, что весь
                                  PresentationFramework построен вокруг данного класса
                                  при создании собственного элемента (не только управления) будет наследоваться базовый класс либо UIElement, либо ContentElement.
                                  и Visual, и Brush и т.д. не являются элементами — это и так понятно. см. предыдущий пункт.
                                  Я буду цитировать данное предложение, пока наконец до вас не дойдет осознание своей неправоты:
                                  Элементом UI в WPF могут быть только классы, наследуемые от DependencyObject, а т.к. inline-элементы (Run, Span, Bold и т.д.) и block-элементы (Paragraph, Section и т.д.) также наследники DependencyObject, следовательно, они также элементы WPF.

                                  Логика меня просто поражает, следовательно, например, DoubleAnimation является элементом?

                                  >>уточню еще больше — относительно своего уже расчитанного места. возможно все недопонимание идет от того, что каждый из нас понимает под словами относительное позиционирование, FlowDocument, элемент.
                                  Относительное позиционирование здесь не причем (хотя здесь у вас тоже были промахи). Я пытаюсь донести до вас то, что вы, назвав инлайны элементами, допустили грубейшую ошибку.
                                    0
                                    >>Элементом управления UI, элементом UI.
                                    Под элементами обычно (я бы даже сказал всегда, но ваши комментарии заставали меня усомниться в этом) подразумеваются, как вы называете, «элементы управления UI».

                                    и правильно сделали, что заставили усомниться.
                                    Я не понимаю, с какого потолка вы взяли «элементы UI», но с, например, кнопкой ContentElement и его наследники ничего общего не имеют.

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

                                    ну теперь все стало на свои места. случай тяжелый. вот представлю все проще:
                                    1. обычно под элементами WPF подразумевают классы, наследуемые от UIElement, чаще FrameworkElement. Это и кнопки, и гриды, и панели и т.д. Это, вижу вы и так знаете. Однако наиболее точно они называются либо элементами управления, либо элементами UI. Именно с ними рабоает layout manager WPF
                                    2. Однако в WPF существует еще отдельный пласт классов, производных от ContentElement, чаще FrameworContentkElement, которые также являются элементами WPF. В чем их предназначение? В основном, в отображении богато отфарматированного текста.
                                      Их главное отличие от элементов UI в том, что они не управляют своим рендерингом. Наоборот, им необходим контейнер (FrameworkElement, например) для отображения своего контента. Для чего нужно? Именно с ними работает Text Rendering Engine. Представьте, если каждый текстовый символ обрабатывался layout manager — к каким тормозам это привело бы.
                                      Тем не менее, данные элементы поддерживают аналогичный набор основных событий, включая события клавиатуры и мыши, drag-and-drop операции, tooltips, инициализацию и могут принимать фокус, т.е. API на 95% схожий.
                                    3. Если Вам интересен принцип работы FrameworContentkElement, и как Text Engine работает, беря на себя пресловутые Measure()/MeasureCore() и Arrange()/ArrangeCore() обратите внимание на namespace MS.Internal.
                                    4. и последнее:
                                      Я буду цитировать данное предложение, пока наконец до вас не дойдет осознание своей неправоты:
                                      Элементом UI в WPF могут быть только классы, наследуемые от DependencyObject, а т.к. inline-элементы (Run, Span, Bold и т.д.) и block-элементы (Paragraph, Section и т.д.) также наследники DependencyObject, следовательно, они также элементы WPF.

                                      помните, я говорил в начале беседы об одной тонкости? так вот DependencyObject просто является наиближайшим parent class и для FrameworkElement, и для FrameworkContentElement.

                                    так что, цитата:
                                    >>Делайте выводы.

                                    P.S.
                                    <сарказм>я Вам: «Вот и Америка», а Вы мне: «Да Индия это, Индия.»</сарказм> ;)
                                      0
                                      Не могу не согласиться с вышесказанным. Думаю на этом можно закончить :)
        0
        > Однако HorizontalOffset и VerticalOffset не работают при использовании FlowDocumentScrollViewer.

        Margin не помог?
          0
          ну, все-таки Margin добавляет отступы, которые были ни к чему в моем случае.
          добиться эффекта как со страницы примера, через margin не получится никак

        Only users with full accounts can post comments. Log in, please.