Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How Can I Get a TextPointer from a mouse click in a FlowDocument

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?

like image 235
yclevine Avatar asked Jan 22 '23 23:01

yclevine


1 Answers

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.

like image 113
Ray Burns Avatar answered May 11 '23 23:05

Ray Burns