Q:

Set tooltip on range of text in wpf richtextbox

I am trying to set a tooltip on an arbitrary range of text on a richtextbox. Is this possible? If so how would I do it e.g by passing in parameters "from" and "to" as (int) indexes. 
Thanks

A:

You could use the following as a starting point:

Add a reference to System.Windows.Interactivity.
Add the following classes to your project:
public class TextRangeToolTip
{
    public int StartPosition { get; set; }

    public int Length { get; set; }

    public object ToolTip { get; set; }

    internal bool IsInRange(int position)
    {
        return this.StartPosition <= position && position < this.StartPosition + this.Length;
    }
}

public class TextRangeToolTipCollection : ObservableCollection<TextRangeToolTip> {}

[ContentProperty("Ranges")]
public class ToolTipBehavior : Behavior<RichTextBox>
{
    private const int ToolTipHideMouseDelta = 9;

    public static readonly DependencyProperty RangesProperty
        = DependencyProperty.Register("Ranges", typeof(TextRangeToolTipCollection),
                                      typeof (ToolTipBehavior),
                                      new PropertyMetadata(OnRangesChanged));

    private readonly DispatcherTimer timer;

    private readonly ToolTip toolTip;
    private Point lastMousePosition;

    public TextRangeToolTipCollection Ranges
    {
        get
        {
            return (TextRangeToolTipCollection)this.GetValue(RangesProperty)
                   ?? (this.Ranges = new TextRangeToolTipCollection());
        }
        set { this.SetValue(RangesProperty, value); }
    }

    public ToolTipBehavior()
    {
        this.Ranges = new TextRangeToolTipCollection();

        this.timer = new DispatcherTimer();
        this.timer.Tick += this.TimerOnTick;
        this.timer.Interval = TimeSpan.FromSeconds(1);

        this.toolTip = new ToolTip {Placement = PlacementMode.Relative};
    }

    protected override void OnAttached()
    {
        this.AssociatedObject.ToolTip = this.toolTip;
        this.toolTip.PlacementTarget = this.AssociatedObject;
        ToolTipService.SetIsEnabled(this.AssociatedObject, false);

        this.AssociatedObject.MouseMove += this.AssociatedObjectOnMouseMove;
    }

    protected override void OnDetaching()
    {
        this.timer.Stop();

        this.toolTip.PlacementTarget = null;
        this.AssociatedObject.ToolTip = null;
        this.AssociatedObject.ClearValue(ToolTipService.IsEnabledProperty);

        this.AssociatedObject.MouseMove -= this.AssociatedObjectOnMouseMove;
    }

    private void AssociatedObjectOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
    {
        Point currentMousePosition = mouseEventArgs.GetPosition(this.AssociatedObject);

        if (this.AssociatedObject.IsMouseCaptured)
        {
            Vector delta = currentMousePosition
                           - this.lastMousePosition;

            if (delta.X*delta.X + delta.Y*delta.Y <= ToolTipHideMouseDelta)
            {
                this.toolTip.HorizontalOffset = currentMousePosition.X + 10;
                this.toolTip.VerticalOffset = currentMousePosition.Y + 10;
                return;
            }

            this.AssociatedObject.ReleaseMouseCapture();
            this.toolTip.IsOpen = false;
        }

        if (this.AssociatedObject.IsMouseOver)
        {
            this.lastMousePosition = currentMousePosition;
            this.timer.Stop();
            this.timer.Start();
        }
    }

    private static void OnRangesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((ToolTipBehavior) d).OnRangesChanged((IEnumerable<TextRangeToolTip>) e.OldValue,
                                              (IEnumerable<TextRangeToolTip>) e.NewValue);
    }

    private void OnRangesChanged(IEnumerable<TextRangeToolTip> oldRanges, IEnumerable<TextRangeToolTip> newRanges)
    {
        var oldObservable = oldRanges as INotifyCollectionChanged;
        if (oldObservable != null)
        {
            CollectionChangedEventManager.RemoveHandler(oldObservable, this.OnRangesCollectionChanged);
        }

        var newObservable = newRanges as INotifyCollectionChanged;
        if (newObservable != null)
        {
            CollectionChangedEventManager.AddHandler(newObservable, this.OnRangesCollectionChanged);
        }

        this.UpdateToolTip();
    }

    private void OnRangesCollectionChanged(
        object sender,
        NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
    {
        this.UpdateToolTip();
    }

    private bool SetToolTipData()
    {
        if (this.Ranges == null)
        {
            return false;
        }

        TextPointer pointer = this.AssociatedObject.GetPositionFromPoint(this.lastMousePosition, false);
        if (pointer == null)
        {
            return false;
        }

        int position = this.AssociatedObject.Document.ContentStart.GetOffsetToPosition(pointer);

        TextRangeToolTip matchingRange = this.Ranges.FirstOrDefault(r => r.IsInRange(position));
        if (matchingRange == null)
        {
            return false;
        }

        this.toolTip.Content = matchingRange.ToolTip;
        return true;
    }

    private void TimerOnTick(object sender, EventArgs eventArgs)
    {
        this.timer.Stop();

        if (this.AssociatedObject.IsMouseOver && this.SetToolTipData())
        {
            this.toolTip.IsOpen = true;
            this.AssociatedObject.CaptureMouse();
        }
    }

    private void UpdateToolTip()
    {
        if (this.AssociatedObject != null && this.AssociatedObject.IsMouseCaptured && !this.SetToolTipData())
        {
            this.toolTip.IsOpen = false;
            this.AssociatedObject.ReleaseMouseCapture();
        }
    }
}

Use it on your RichTextBox like this:
<RichTextBox>
    <i:Interaction.Behaviors>
        <myapp:ToolTipBehavior>
            <myapp:TextRangeToolTip StartPosition="10" Length="4" ToolTip="some" />
            <myapp:TextRangeToolTip StartPosition="15" Length="4" ToolTip="text" />
        </myapp:ToolTipBehavior>
    </i:Interaction.Behaviors>
    <FlowDocument>
        <Paragraph>This is some text. This is some other text.</Paragraph>
    </FlowDocument>
</RichTextBox>

Alternatively, you can bind a TextRangeToolTipCollection to the Ranges property like this:
<RichTextBox Document="{Binding Document}">
    <i:Interaction.Behaviors>
        <myapp:ToolTipBehavior Ranges="{Binding RangeToolTips}" />
    </i:Interaction.Behaviors>
</RichTextBox>

Getting the positions right is a bit tricky, because WPF counts symbols, not characters. You could extend the TextRangeToolTip class to have properties of type TextPointer or TextRange and construct it using your FlowDocument instance.

