State Street Gang
.NET, straight up

Scroll Selected Text Into View in WPF

May 2, 2008 17:08 by Will

(Illness and work has been kicking my ass for the past couple weeks; I'm posting a quick how-to so my parents don't worry.  I'll be finishing up the crypto series next week, I swear!)

I've been doing lots of work in WPF over the last few weeks, getting familiar with xaml for a major project at work (xaml templating and XPS documents, cool stuff).  Getting to re-know standard windows forms controls in their new WPF flavors has been interesting.  Every once in awhile you come on an issue that takes a while to figure out.

One of those I just encountered was getting the WPF version of the TextBox to scroll a particular chunk of text into view.  Here's the scenario:

  • Text is pasted into a multiline TextBox without wrapping that overflows the available visual space
  • Both horizontal and vertical scrollbars appear
  • In code, a word is selected.  That word happens to be outside the visible area of the TextBox.

In this situation, the question becomes how to scroll this selected text into view via code so the user can see the selected text.

The standard methods don't work.  The obvious methods for scrolling (I'll skip them for brevity's sake) only scroll the TextBox vertically, or take a value that cannot easily and reliably be determined from the available information. 

The solution is to use the BringIntoView method.  The problem is that this method is designed to communicate to an owning ScrollView in order to tell it to scroll.  If you call this on the TextBox and the TextBox is using an internal ScrollView, the method fails to work.

You must turn off your TextBox' scrolling and place the TextBox into a ScrollView.  Here's the xaml:

<Border
  DockPanel.Dock="Top"
  BorderBrush="Gray"
  BorderThickness="1">
  <ScrollViewer
   VerticalScrollBarVisibility="Auto"
   HorizontalScrollBarVisibility="Auto">
    <TextBox
     BorderThickness="0"
     Name="_inputString"
     AcceptsReturn="True"
     TextWrapping="NoWrap"
     GotMouseCapture="TextBox_GotFocus"
     Text="{Binding Path=InputString, 
        UpdateSourceTrigger=PropertyChanged}">
    </TextBox>
  </ScrollViewer>
</Border>

Notice the Border; the ScrollView doesn't display a border, so you have to create one and turn off the TextBox' so it doesn't look weird.  Here's the codebehind:

_inputString.Select(m.Index, m.Length);
Rect start = _inputString
  .GetRectFromCharacterIndex(m.Index, true);
Rect end = _inputString
  .GetRectFromCharacterIndex
    (m.Index + m.Length, true);
_inputString.BringIntoView(Rect.Union(start, end));
_inputString.Focus();

The m object in this case is actually a Regex Match object and _inputString is the TextBox; the code is from a databound wpf regex tool I'm working on.  I can select text using the start index of the text and the length of the selection.

The BringIntoView method takes a Rect struct.  The GetRectFromCharacterIndex method returns a Rect that covers an individual character of the text; we want to bring the whole selection into view so we need the union of the Rect from the first and last characters of the selection.

Its a little sloppy.  I haven't done much testing on this; guaranteed that if the first character Rect is on line 278 and the last is on line 279 something odd might happen, but it does work.

Update

Adding this to the scrolling code allows you to scroll to the first half of the text if the selection contains a newline:

if (_inputString.SelectedText.Contains(Environment.NewLine))
{
    string selected = _inputString.SelectedText;
    int length = selected.IndexOf(Environment.NewLine);
    end = _inputString.GetRectFromCharacterIndex
        (m.Index + length, true);
}
else
{
    end = _inputString.GetRectFromCharacterIndex
        (m.Index + m.Length, true);
}
kick it on DotNetKicks.com
Tags:
Categories: Tips | WPF
Actions: E-mail | Permalink | Comments (0) | Comment RSSRSS comment feed

Related posts

Comments are closed