views:

144

answers:

2

I would like to get the word that a user has clicked on in a FlowDocument.

I am currently adding an event handler to every Run in the document and iterating through the TextPointers in the Run that was clicked, calling GetCharacterRect() on each one and checking if the rectangle contains the point.

However, when the click occurs near the end of a long Run this takes > 10 seconds.

Is there any more efficient method?

+2  A: 

I'd say the easiest way is to use the Automation interfaces:

using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;

FlowDocument flowDocument = ...;
Point point = ...;

var peer = new DocumentAutomationPeer(flowDocument);
var textProvider = (ITextProvider)peer.GetPattern(PatternInterface.Text);
var rangeProvider = textProvider.RangeFromPoint(point);

The ITextProvider usage requires a reference to the UIAutomationProvider assembly. This assembly is not commonly referenced, so you may need to add it. UIAutomationTypes will also be needed to use some of its methods.

Note that there are many options for creating your automation peer depending on how you are presenting the FlowDocument:

var peer = new DocumentAutomationPeer(flowDocument);
var peer = new DocumentAutomationPeer(textBlock);
var peer = new DocumentAutomationPeer(flowDocumentScrollViewer);
var peer = new TextBoxAutomationPeer(textBox);
var peer = new RichTextBoxAutomationPeer(richTextBox);

Update

I tried this and it works well, though converting from an ITextRangeProvider to a TextPointer proved more difficult than I expected.

I packaged the algorithm in an extension method ScreenPointToTextPointer for easy use. Here is an example of how my extension method can be used to bold all text before the mouse pointer and un-bold all text after it:

private void Window_MouseMove(object sender, MouseEventArgs e)
{
  var document = this.Viewer.Document;
  var screenPoint = PointToScreen(e.GetPosition(this));

  TextPointer pointer = document.ScreenPointToTextPointer(screenPoint);

  new TextRange(document.ContentStart, pointer).ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold);
  new TextRange(pointer, document.ContentEnd).ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Normal);
}

Here is the code for the extension method:

using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
using System.Windows.Automation.Text;

public static class DocumentExtensions
{
  // Point is specified relative to the given visual
  public static TextPointer ScreenPointToTextPointer(this FlowDocument document, Point screenPoint)
  {
    // Get text before point using automation
    var peer = new DocumentAutomationPeer(document);
    var textProvider = (ITextProvider)peer.GetPattern(PatternInterface.Text);
    var rangeProvider = textProvider.RangeFromPoint(screenPoint);
    rangeProvider.MoveEndpointByUnit(TextPatternRangeEndpoint.Start, TextUnit.Document, 1);
    int charsBeforePoint = rangeProvider.GetText(int.MaxValue).Length;

    // Find the pointer that corresponds to the TextPointer
    var pointer = document.ContentStart.GetPositionAtOffset(charsBeforePoint);

    // Adjust for difference between "text offset" and actual number of characters before pointer
    for(int i=0; i<10; i++)  // Limit to 10 adjustments
    {
      int error = charsBeforePoint - new TextRange(document.ContentStart, pointer).Text.Length;
      if(error==0) break;
      pointer = pointer.GetPositionAtOffset(error);
    }
    return pointer;
  }

}

Also note the use of PointToScreen in the example MouseMove method to get a screen point to pass into the extension method.

Ray Burns
This seems like an interesting solution. The RangeFromPoint method seems to require a point in screen (absolute) coordinates as opposed to the relative coordinates provided by the mouse event. How would I convert the coordinates?
Bear
Two issues with this solution:1. range.GetText() is returning a zero-length string.2. If I did get a string, how would I know which word in the string corresponds to the location of the mouse click?
Bear
Good questions. I've added working code to my answer that shows exactly how to do all of these things.
Ray Burns
Thank you! Exactly what I needed.
Bear
A: 

Mouse Click events are bubbled to the top, instead you can simply hook PreviewMouseLeftButtonUp in your document and watch for the sender/original source of the event, you will get the Run that sent you the event.

Then you can RangeFromPoint and you can use,

PointToScreen that will convert your local mouse point to global point.

Akash Kava
I can already easily get the Run. The problem is getting the TextPointer closest to the click.
Bear
Run is derived from TextElement and it has ContentStart and ContentEnd which could be nearest TextPointer, are you looking for TextPointer inside Run?
Akash Kava
Yes. I need the exact TextPointer, so that I can figure out the word that was clicked and provide additional information (dictionary lookup) about that word.
Bear
Thank you for the helpful update. See my comment on Ray's answer, though.
Bear