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?
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.
If the FlowDocument is that of a RichTextBox, you could use the GetPositionFromPoint() method to get the TextPointer.
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.