[Tapuz .Net] Getting WPF's FlowDocument and FlowDoucmentReader MouseOver text

29 בינואר 2008

2 תגובות

Question:

I've got a WPF FlowDocument in a FlowDocumentReader.
On mouse left button click I need to get the "word" that the user clicked on.
How can I get that "word"?

 

Answer:

First, let's understand the problem.

To those of you not familiar with the WPF FlowDocument check out this XAML file: www.sneath.org/tim/chocolate.xaml
If you've got .Net 3.0 installed on your machine, it will automatically render and run it inside your browser.
image

We can change the Font-size with the top-right scroller.

image 
Fundamentally the FlowDocument with it's various presenters is meant to display real-world readable text.

So let's see what the problem is.

Let's add a new WPF XAML window.

image

image

We get the following XAML in our empty window.

<Window x:Class="WPFTesting.Window1"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   Title="Window1" Height="344" Width="453">

    <Grid>

 

    </Grid>

</Window>

Let's add a new FlowDoucmentReader.

<Window x:Class="WPFTesting.Window1"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   Title="Window1" Height="344" Width="453">

    <Grid>

        <FlowDocumentReader x:Name="flowRdr1">

 

        </FlowDocumentReader>

    </Grid>

</Window>

And we will add a random FlowDocument in our FlowDocumentReader. Let's copy the Example FlowDocument from MSDN2.

<Window x:Class="WPFTesting.Window1"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   Title="Window1" Height="344" Width="453">

    <Grid>

        <FlowDocumentReader x:Name="flowRdr1">

            <FlowDocument>

                <Paragraph FontSize="18">Flow Format Example</Paragraph>

 

                <Paragraph>

                    Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy

      nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi

      enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis

      nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure.

                </Paragraph>

                <Paragraph>

                    Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh

      euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim

      ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl

      ut aliquip ex ea commodo consequat. Duis autem vel eum iriure.

                </Paragraph>

 

                <Paragraph FontSize="18">More flow elements</Paragraph>

                <Paragraph FontSize="15">Inline, font type and weight, and a List</Paragraph>

 

                <List>

                    <ListItem>

                        <Paragraph>ListItem 1</Paragraph>

                    </ListItem>

                    <ListItem>

                        <Paragraph>ListItem 2</Paragraph>

                    </ListItem>

                    <ListItem>

                        <Paragraph>ListItem 3</Paragraph>

                    </ListItem>

                    <ListItem>

                        <Paragraph>ListItem 4</Paragraph>

                    </ListItem>

                    <ListItem>

                        <Paragraph>ListItem 5</Paragraph>

                    </ListItem>

                </List>

 

                <Paragraph>

                    <Bold>Bolded</Bold>

                </Paragraph>

                <Paragraph>

                    <Underline>Underlined</Underline>

                </Paragraph>

                <Paragraph>

                    <Bold>

                        <Underline>Bolded and Underlined</Underline>

                    </Bold>

                </Paragraph>

                <Paragraph>

                    <Italic>Italic</Italic>

                </Paragraph>

 

                <Paragraph>

                    <Span>The Span element, no inherent rendering</Span>

                </Paragraph>

                <Paragraph>

                    <Run>The Run element, no inherent rendering</Run>

                </Paragraph>

 

                <Paragraph FontSize="15">Subscript, Superscript</Paragraph>

 

                <Paragraph>

                    <Run Typography.Variants="Superscript">This text is Superscripted.</Run> This text isn't.

                </Paragraph>

                <Paragraph>

                    <Run Typography.Variants="Subscript">This text is Subscripted.</Run> This text isn't.

                </Paragraph>

                <Paragraph>

                    If a font does not support a particular form (such as Superscript) a default font form will be displayed.

                </Paragraph>

 

                <Paragraph FontSize="15">Blocks, breaks, paragraph</Paragraph>

 

                <Section>

                    <Paragraph>A block section of text</Paragraph>

                </Section>

                <Section>

                    <Paragraph>Another block section of text</Paragraph>

                </Section>

 

                <Paragraph>

                    <LineBreak/>

                </Paragraph>

                <Section>

                    <Paragraph>… and another section, preceded by a LineBreak</Paragraph>

                </Section>

 

                <Section BreakPageBefore="True"/>

                <Section>

                    <Paragraph>… and another section, preceded by a PageBreak</Paragraph>

                </Section>

 

                <Paragraph>Finally, a paragraph. Note the break between this paragraph …</Paragraph>

                <Paragraph TextIndent="25">… and this paragraph, and also the left indention.</Paragraph>

 

                <Paragraph>

                    <LineBreak/>

                </Paragraph>

 

            </FlowDocument>

        </FlowDocumentReader>

    </Grid>

</Window>

In our Visual Studio 2008 Designer we'll see that our window looks like the following image.

image

And we can run it and see pretty much the exact same thing.

image

We can work with it like with any other FlowDocumentReader.

Here's me changing pages.

image

Resizing the Font-size.

image 

And so on.

 

The question refers to getting the text on the FlowDocumentReader in the current mouse position.

If we were working with a TextBlock or a RichTextBox we could have used the GetPositionFromPoint method.

image

But we don't have a FlowDocumentReader.GetPositionFromPoint method.

So I wrote up one quickly.

    public static class RunExtensions

    {

        public static TextPointer GetPositionFromPoint(/* this */ Run _this, Point searchForPoint)

        {

            TextPointer ptrCurCharcter = _this.ContentStart.GetNextInsertionPosition(LogicalDirection.Forward);

            TextPointer ptrNextCharcter = ptrCurCharcter.GetNextInsertionPosition(LogicalDirection.Forward);

            while (ptrNextCharcter != null)

            {

                Rect charcterInsertionPointRectangle = ptrCurCharcter.GetCharacterRect(LogicalDirection.Forward);

                Rect nextCharcterInsertionPointRectangle = ptrNextCharcter.GetCharacterRect(LogicalDirection.Backward);

 

                if (searchForPoint.X >= charcterInsertionPointRectangle.X

                    && searchForPoint.X <= nextCharcterInsertionPointRectangle.X

                    && searchForPoint.Y >= charcterInsertionPointRectangle.Top

                    && searchForPoint.Y <= charcterInsertionPointRectangle.Bottom)

                {

                    return ptrCurCharcter;

                }

 

                ptrCurCharcter = ptrCurCharcter.GetNextInsertionPosition(LogicalDirection.Forward);

                ptrNextCharcter = ptrNextCharcter.GetNextInsertionPosition(LogicalDirection.Forward);

            }

 

            return null;

        }

    }

This method gets a Point and checks if there's a TextPointer on the "System.Windows.Documents.Run" that is in the bounds of that point.

Now we've got to hook this up to the FlowDocumentReader.
I'm using a few Logical Tree Helper methods I've got on the side.

    public static class LogicalTreeHelperHelper

    {

            public static IEnumerable GetChildren(DependencyObject obj, Boolean AllChildrenInHierachy)

            {

                if (!AllChildrenInHierachy)

                    return LogicalTreeHelper.GetChildren(obj);

 

                else

                {

                    List<object> ReturnValues = new List<object>();

 

                    RecursionReturnAllChildren(obj, ReturnValues);

 

                    return ReturnValues;

                }

            }

 

        private static void RecursionReturnAllChildren(DependencyObject obj, List<object> returnValues)

        {

            foreach (object curChild in LogicalTreeHelper.GetChildren(obj))

            {

                returnValues.Add(curChild);

                if (curChild is DependencyObject)

                    RecursionReturnAllChildren((DependencyObject)curChild, returnValues);

            }

        }

 

        public static IEnumerable<ReturnType> GetChildren<ReturnType>(DependencyObject obj, Boolean AllChildrenInHierachy)

        {

            foreach (object child in GetChildren(obj, AllChildrenInHierachy))

                if (child is  ReturnType)

                    yield return (ReturnType)child;

        }

    }

Real quick – this GetChildren method gets a bool saying if it's to return all children in the class hierarchy or just the first level one.
The overloaded Generic GetChildren gets a generic type "ReturnType" and filters all the children based on that type.

 

One way to get the TextPointer is by registering to the FlowDocumentReader.MouseDown event.

<Window x:Class="WPFTesting.Window1"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   Title="Window1" Height="344" Width="453">

    <Grid>

        <FlowDocumentReader x:Name="flowRdr1">

            <FlowDocument MouseDown="FlowDocument_MouseDown">

                …

            </FlowDocument>

        </FlowDocumentReader>

    </Grid>

</Window>

And on the MouseDown event call a method that iterates over all children of type "Run" and uses GetPositionFromPoint with the current mouse position. 

        private void FlowDocument_MouseDown(object sender, MouseButtonEventArgs e)

        {

            Point mousePosition = Mouse.GetPosition(this.flowRdr1);

            TextPointer ptr = FlowDocumentReaderExtensions.GetPositionFromPoint(flowRdr1, mousePosition);

            if (ptr != null)

            {

                string textAfterCursor = ptr.GetTextInRun(LogicalDirection.Forward);

                string textBeforeCursor = ptr.GetTextInRun(LogicalDirection.Backward);

            }

        }

All the FlowDocumentExtensions.GetPositionFromPoint does is iterate over all Run inlines on the FlowDocument and checks their RunExtensions.GetPositionFromPoint.

    public static class FlowDocumentReaderExtensions

    {

        public static TextPointer GetPositionFromPoint(/*this*/ FlowDocumentReader _this, Point searchForPoint)

        {

            foreach (Run curRun in LogicalTreeHelperHelper.GetChildren<Run>(_this, true))

            {

                TextPointer ptr = RunExtensions.GetPositionFromPoint(curRun, searchForPoint);

                if (ptr != null)

                    return ptr;

            }

 

            return null;

        }

    }

 

Let's run this and Click on the "U" in "Lorm Ipsum".

image

Using the debugger we can see the values we get from FlowDocument.GetPositionFromPoint.

image

image

Using these two strings, we can get the current "word" the mouse is over.

        private void FlowDocument_MouseDown(object sender, MouseButtonEventArgs e)

        {

            Point mousePosition = Mouse.GetPosition(this.flowRdr1);

            TextPointer ptr = FlowDocumentReaderExtensions.GetPositionFromPoint(flowRdr1, mousePosition);

            if (ptr != null)

            {

                string textAfterCursor = ptr.GetTextInRun(LogicalDirection.Forward);

                string textBeforeCursor = ptr.GetTextInRun(LogicalDirection.Backward);

 

                string[] textsAfterCursor = textAfterCursor.Split('.', ' ');

                string[] textsBeforeCursor = textBeforeCursor.Split('.', ' ');

 

                string currentWord = textsBeforeCursor[textsBeforeCursor.Length - 1] + textsAfterCursor[0];

 

            }


image 

 

Tamir Khason suggested a good way to improve performance on this one.
We can
attach a MouseDown event to each Run before hand and thus save the recursion over the logical WPF control tree.

        private void Window_Loaded(object sender, RoutedEventArgs e)

        {

            foreach (Run curRun in LogicalTreeHelperHelper.GetChildren<Run>(flowRdr1.Document, true))

                curRun.MouseDown += new MouseButtonEventHandler(curRun_MouseDown);

        }

 

        void curRun_MouseDown(object sender, MouseButtonEventArgs e)

        {

            Point mousePosition = Mouse.GetPosition(this.flowRdr1);

           Run senderRun = (Run) sender;

            TextPointer ptr = RunExtensions.GetPositionFromPoint(senderRun, mousePosition);

            if (ptr != null)

            {

                string textAfterCursor = ptr.GetTextInRun(LogicalDirection.Forward);

                string textBeforeCursor = ptr.GetTextInRun(LogicalDirection.Backward);

 

                string[] textsAfterCursor = textAfterCursor.Split('.', ' ');

                string[] textsBeforeCursor = textBeforeCursor.Split('.', ' ');

 

                string currentWord = textsBeforeCursor[textsBeforeCursor.Length - 1] + textsAfterCursor[0];

 

            }

        }

 

You can download all the sample code at – www.JustinAngel.Net/files/Example-WPF-FlowDocument-GetPositionFromPoint.zip.

 

Link: http://www.tapuz.co.il/tapuzforum/main/Viewmsg.asp?forum=831&msgid=112005511

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. (*) שדות חובה מסומנים

2 תגובות

  1. Justin-Josef Angel [MVP]29 בינואר 2008 ב 18:57

    Tamir khason is a WPF GOD!

    Tamir, you already had your name and a link in the article since it was published :)

    להגיב